Life of an HTTP request in a Go server

Related Articles

Go is a common tool suitable for writing HTTP servers. This post discusses the route that a typical HTTP request goes through through a Go server, and deals with routers, middleware, and other related issues as in parallel.

So that you have some concrete code to look at, let’s start with this trivial server (which is a client from it servers):

package main

import (

func hello(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "hellon")

func headers(w http.ResponseWriter, req *http.Request) 
  for name, headers := range req.Header 
    for _, h := range headers 
      fmt.Fprintf(w, "%v: %vn", name, h)

func main() 
  http.HandleFunc("/hello", hello)
  http.HandleFunc("/headers", headers)

  http.ListenAndServe(":8090", nil)

We will begin to trace the life of an HTTP request through this server by looking at http.ListenAndServe function:

func ListenAndServe(addr string, handler Handler) error

A simple flow of what happens when it is read is shown in this diagram:

This is a heavily “embedded” version of the actual sequence of function calls and method, but The original code Not too hard to follow.

The main flow is about what you would expect: ListenAndServe Listens to the TCP port for the given address, and then loops gets new connections. For each new essay he rotates Gorotin to submit it (more on that below). The submission of the essay includes a loop of:

  • Analyze HTTP request from connection; produce http
  • Pass the http For a user-defined therapist

A therapist is anything that implements the http.Handler interface:

type Handler interface 
    ServeHTTP(ResponseWriter, *Request)

The default handler

In our sample code, ListenAndServe Powered by zero As the second argument, which should be the user-defined handler. What’s going on?

Our diagram simplifies some details; In reality, when the HTTP package requests, it does not directly call the user’s handler but uses this adapter:

type serverHandler struct 
  srv *Server

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) 
  handler := sh.srv.Handler
  if handler == nil 
    handler = DefaultServeMux
  if req.RequestURI == "*" && req.Method == "OPTIONS" 
    handler = globalOptionsHandler
  handler.ServeHTTP(rw, req)

Notice the highlighted lines: If Handler == Zero, Then http.DefaultServeMux
Serves as a therapist. It Default server mux Global show of the http.ServeMux Type held in http package. By the way, when our sample code impresses therapists with http.HandleFunc, It registers them on the same default mux.

We can rewrite our sample server as follows, without using the mux default. Only the main Function changes, so I will not see the Peace and
Headlines Caregivers, but you can see the Full code here. No change in functionality:

func main() 
  mux := http.NewServeMux()
  mux.HandleFunc("/hello", hello)
  mux.HandleFunc("/headers", headers)

  http.ListenAndServe(":8090", mux)

A ServeMux He is only a Therapist

When reading a lot of examples of Go’s servers it’s easy to believe
ListenAndServe “Takes Mox” as an argument, but it is not accurate. As we saw above, what ListenAndServe Takes is a value to implement the
http.Handler interface. We can write the following server without any moxibustion:

type PoliteServer struct 

func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "Welcome! Thanks for visiting!n")

func main() 
  ps := &PoliteServer
  log.Fatal(http.ListenAndServe(":8090", ps))

There is no routing here; All HTTP requests go to HTTP server Method of
PoliteServer And he replies with the same message to everyone. try
curl– Running this server with different paths and methods; It’s nothing if not consistent.

We can simplify our polite server even more by using http.HandlerFunc:

func politeGreeting(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "Welcome! Thanks for visiting!n")

func main() 
  log.Fatal(http.ListenAndServe(":8090", http.HandlerFunc(politeGreeting)))

where HandlerFunc Does this smart adapter live in http package:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) 
  f(w, r)

If you pay attention http.HandleFunc In the first example in the post, he uses the same adapter for functions that have the HandlerFunc signature.

Just like PoliteServer, http.ServeMux Is the type that implements the
http.Handler interface. You can browse it Full code If you want; Here is an outline:

  • ServeMux Keeps a slice sorted (by length) of Template, handler Couples.
  • handle or HandleFunc Adds a new therapist to the slice
  • HTTP server:
    • Finds the handler for the request path (by searching the sorted pair of slices)
    • Calling the therapist HTTP server method

As such, the mux can be seen as a Handles shipping; This pattern is most common in HTTP server programming. It Intermediate software.

http.Handler Intermediate tool

Medium software is difficult to define precisely because it means slightly different things in different contexts, languages ​​and frameworks.

Let’s take the flow diagram from the beginning of this post and simplify it a bit, and hide the details of what http The package does:

http.ListenAndServe Even simpler flow

Now, this is what the flow looks like when we add intermediate software.

http.ListenAndServe Flow with Intermediate Software

In Go, the middleware is just another HTTP handler that wraps around another handler. The mediated therapist is registered to be called by ListenAndServe; When called, it can perform arbitrary pre-processing, call the wrapper handler and then perform arbitrary post-processing.

We have seen above one example of mediation – http.ServeMux; In this case, the pre-processing is the selection of the appropriate therapist for the user to call, based on the path of the request. No processing after.

For another concrete example, let’s visit our polite server again and add some basic
Intermediate Registration Software. This middleware records details of each request, including how long it took to execute:

type LoggingMiddleware struct 
  handler http.Handler

func (lm *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) 
  start := time.Now()
  lm.handler.ServeHTTP(w, req)
  log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))

type PoliteServer struct 

func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "Welcome! Thanks for visiting!n")

func main() 
  ps := &PoliteServer
  lm := &LoggingMiddlewarehandler: ps
  log.Fatal(http.ListenAndServe(":8090", lm))

Notice how Middle software registration He himself an http.Handler Which holds the user handler as a field. When ListenAndServe Calls it HTTP server The method, it does:

  1. Preliminary processing: Write a timestamp before activating the therapist
  2. Call the user therapist with the request and answer writer
  3. Post-processing: Record the application details, along with the elapsed time

The great thing about the middleware is that it is connectable. The “user handler” that wraps in the medium can be another medium, and so on. This is a necklace of
http.Handler Values ​​that envelop each other. In fact, this is a common pattern in Go, which leads us to how Go’s mid-range software usually looks. Here’s our polite registry server again, with a more recognizable Go middleware application:

func politeGreeting(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "Welcome! Thanks for visiting!n")

func loggingMiddleware(next http.Handler) http.Handler 
  return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) 
    start := time.Now()
    next.ServeHTTP(w, req)
    log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))

func main() 
  lm := loggingMiddleware(http.HandlerFunc(politeGreeting))
  log.Fatal(http.ListenAndServe(":8090", lm))

Instead of creating a structure with a method, Middle software registration Cranes
http.HandlerFunc In combination with a Closure To make the code much more concise, while maintaining the same functionality. More importantly, it demonstrates a de facto standard signature For intermediate software: a function that takes a http.Handler, Sometimes together with another country, and returns differently http.Handler. The returned therapist should now be used in place of the therapist transferred to the mediator, and he will “magically” perform his original functionality wrapped in the functionality of the middleware.

For example, the standard directory includes the following software:

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

So if we have a http.Handler In our code, wrap it like this:

handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out")

Creates a new version of the therapist with a built-in 2-second time-out mechanism.

The assembly capability of the intermediate software can be demonstrated as follows:

handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out")
handler = loggingMiddleware(handler)

After these two lines, Therapist There will be timeouts and Registration Registration is installed. You may notice that long chains of middleware can be tedious to write; Go has a number of popular packages to deal with, but it’s out of the scope of this post.

By the way, even the http The package uses intermediate software internally for its needs; See for example the serverHandler A correlation described earlier in this post. It provides a clean way to treat zero Handlers use by default (by forwarding the request to the default mux).

I hope this clarifies why Intermediate Software is an attractive design tool. We can work on our “business logic” therapists, while in a completely orthogonal way we leverage generic middleware that can improve our therapists in many ways. However, I will leave here a more complete examination of the possibilities for another post.

Parallel treatment and panic

To conclude our investigation into what goes through an HTTP request on a Go HTTP server, let’s review two more issues: parallel and panic management.

First, in parallel. As I mentioned briefly above, each connection is treated with a new guru by http.Server.Serve.

This is a powerful feature of Go’s net / http, Since it leverages Go’s excellent parallel capabilities, with inexpensive gurotines that ensure a very clean parallelism model for HTTP handlers. A therapist can block (for example, by reading from a database) without worrying about delaying other therapists. However, this requires some caution in writing from therapists who have shared data. Read my previous post for more details.

Finally, Panic treatment. An HTTP server is usually designed to be a long process in the background. Suppose something bad happens to a particular user-provided therapist, for example some bug that leads to a run-time panic. This could crash the entire server, which is not a great scenario. To protect yourself from this, you may want to consider adding a recover To your server main Function, but it will not help for several reasons:

  1. Until control returns to main, ListenAndServe Has already been done so that no more submission will occur.
  2. Because each connection is treated with a separate gurotina, therapists’ panic will not even come main But instead will collapse the process.

To provide some protection against this, net / http There is a built-in restore for each Gorotin dose (in conn.serve method). We can see this in action with a simple example:

func hello(w http.ResponseWriter, req *http.Request) 
  fmt.Fprintf(w, "hellon")

func doPanic(w http.ResponseWriter, req *http.Request) 

func main() 
  http.HandleFunc("/hello", hello)
  http.HandleFunc("/panic", doPanic)

  http.ListenAndServe(":8090", nil)

If we run this server and curl goddess /panic Path, let’s see it:

$ curl localhost:8090/panic
curl: (52) Empty reply from server

And the server will print it to his log:

2021/02/16 09:44:31 http: panic serving oops
goroutine 8 [running]:
  /usr/local/go/src/net/http/server.go:1801 +0x147
panic(0x654840, 0x6f0b80)
  /usr/local/go/src/runtime/panic.go:975 +0x47a
main.doPanic(0x6fa060, 0xc0001401c0, 0xc000164200)
[... rest of stack dump here ...]

However, the server will continue to operate and we will be able to contact it!

Although this built-in protection is preferable to server crashes, many developers find it somewhat limiting. All he does is close the connection and connect to the error; Often, it is more useful to return some sort of error response to the client (such as code 500 – internal server error) with more details.

After reading this post, it should be easy to write an intermediate program that manages to do this. Try it as an exercise! I will cover this use case in a future post.



Please enter your comment!
Please enter your name here

Popular Articles