Go language is a common and very suitable tool for writing HTTP services. This blog post discusses the routing of a typical HTTP request through a go service, involving routing, middleware and related issues such as concurrency.
In order to have specific code for reference, let's start with this simple service code (from Go by Example: HTTP Servers)
package main import ( "fmt" "net/http" ) func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello\n") } func headers(w http.ResponseWriter, req *http.Request) { for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, "%v: %v\n", name, h) } } } func main() { http.HandleFunc("/hello", hello) http.HandleFunc("/headers", headers) http.ListenAndServe(":8090", nil) }
We'll check http.ListenAndServe Function to start tracking the life cycle of an HTTP request in this service:
func ListenAndServe(addr string, handler Handler) error
This figure shows the brief process that occurs during the call:
This is a highly "inline" version of the actual sequence of function and method calls, but Original code It's not hard to understand.
The main process is what you expect: ListenAndServe Listen to the TCP port of the given address, and then cycle to accept new connections. For each new connection, it will schedule a goroutine to process the connection (described in detail later). Processing the connection involves such a cycle:
- Parsing HTTP requests from connections; generating http.Request
- Put this http.Request Passed to user-defined handler
handler is an implementation http.Handler Any instance of the interface:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Default handler
In our example code, ListenAndServe Used when called nil As the second parameter, this location should have used a user-defined handler. What's going on?
Our diagram simplifies some details; in fact, when the HTTP package processes a request, it does not directly call the user's handler, but uses the 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) }
Note the highlighted part (if handler == nil...), if handler == nil, then http.DefaultServeMux Used as a handler. This is the default server mux, http One contained in the package http.ServeMux By the way, when our sample code uses http.HandleFunc When registering the handler function, these handlers will be registered on the same default mux.
We can rewrite our sample code as follows, no longer using the default mux. Just modify main Function, so it's not shown here hello and headers handler function. We can see it here Complete code [^ 1]:
func main() { mux := http.NewServeMux() mux.HandleFunc("/hello", hello) mux.HandleFunc("/headers", headers) http.ListenAndServe(":8090", mux) }
One ServeMux Just a Handler
When you look at many examples of Go service, it's easy to give people an impression ListenAndServe The function "needs a mux" as an argument, but this is inaccurate. As we saw before, listenandserve What the function needs is an implementation http.Handler Interface value. We can write the following services without any mux:
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)) }
Because there is no routing logic here; all arrivals PoliteServer of Serve HTTP Method will reply with the same message. Try using different paths and methods curl - ing this service; the return must be consistent.
We can use http.HandlerFunc To further simplify our polite service:
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))) }
HandlerFunc It's such a place http Smart adapter in 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 notice in the first example of this blog post http.HandleFunc[^2], which has HandlerFunc Signed functions use the same adapter.
Like PoliteServer Same as http.servermux It's an implementation http.Handler The type of interface. You can read it carefully if you like Complete code ; this is an outline:
- ServeMux A sorted by length is maintained {pattern, handler} Slice of.
- Handle or HandleFunc Add a new handler to the slice.
-
ServeHTTP:
- (by looking up the slice of the sorted handler pair) find the corresponding handler for the requested path
- Calling handler ServeHTTP method
Therefore, mux can be regarded as a forwarding handler; this pattern is very common in HTTP service development, which is middleware.
http.Handler middleware
Because middleware means different things in different contexts, different languages and different frameworks, it is difficult to be accurately defined.
Let's go back to the flowchart at the beginning of this blog post and simplify and hide it a little http Details of package execution:
Now, when we add middleware, the flow chart looks like this:
In Go language, middleware is just another HTTP handler, which wraps another handler. Middleware handler calls ListenAndServe Is registered; when called, it can perform any preprocessing, call its own package handler, and then perform any post-processing.
We have seen an example of middleware before—— Http. Servermux; in this example, preprocessing is to select the correct user handler to call based on the requested path. There is no post-processing.
Another specific example is to go back to our polite service and add some basic logging middleware. This middleware records the specific log of each request, including the execution time:
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 := &LoggingMiddleware{handler: ps} log.Fatal(http.ListenAndServe(":8090", lm)) }
be careful LoggingMiddleware Itself is a http.Handler, which holds a user handler as a field ListenAndServe Call its ServeHTTP Method, which does the following:
- Preprocessing: record a timestamp before the user's handler executes
- Call the user handler with the request and return writer
- Post processing: record the detailed request log, including time-consuming
The biggest advantage of middleware is that it can be combined. The "user handler" wrapped by middleware can also be another middleware, and so on. This is a mutual wrapping http.Handler Chain. In fact, this is a common pattern in Go. Let's take a look at the classic usage of Go middleware. Our log poly service uses a more recognizable Go middleware implementation this time:
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 methods, loggingMiddleware utilize http.HandlerFunc And closures make the code more concise and retain the same function. More importantly, this example shows the de facto standard signature of middleware: a function passes in a function http.Handler, and sometimes other states, and then return a different http.Handler. The returned handler should now replace the handler passed into the middleware, and then it will "magically" perform its original functions and be packaged with the functions of the middleware.
For example, the standard library contains the following middleware:
func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
If our code has http.Handler, wrap it like this:
handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out")
A new version of handler is created, which has a built-in 2-second timeout mechanism.
The combination of middleware can be shown as follows:
handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out") handler = loggingMiddleware(handler)
After two lines of code, handler It will have timeout and logging functions. You may notice that middleware with long links is cumbersome to write; Go has many popular packages that can solve this problem, but this is beyond the scope of this article.
By the way, although http The package uses middleware internally to meet its own needs; For details, see the previous post on serverHandler Examples of adapters. However, it provides a clear way to handle the case where the user handler is nil with the default behavior (pass the request into the default mux).
I hope this can let you understand why middleware is a very attractive CAD. We can focus on our "business logic" handler. Although it is completely orthogonal, we use general middleware to improve our handler in many aspects. In other articles, it will be discussed comprehensively.
Concurrency and panic processing
To conclude our exploration of HTTP requests in the Go HTTP service, let's introduce two other topics: concurrency and panic processing.
The first is concurrency. As mentioned earlier, each connection is made by http.Server.Serve Process in a new goroutine.
This is Go's net/http It takes advantage of the excellent concurrency performance of Go and uses the lightweight goroutine to maintain a very simple concurrency model for HTTP handler. When a handler is blocked (for example, reading a database), there is no need to worry about slowing down other handlers. However, you need to be careful when writing a handler that has shared data. Refer to for details Previous articles.
Finally, panic processing. An HTTP service is usually a long-running background process. If something bad happens in the request handler provided by the user, for example, some bug s that lead to runtime panic. It will cause the whole service to crash, which is not a good thing. In order to avoid such a tragedy, you may consider in your service main Add to function recover, but it is useless for the following reasons:
- When control is returned to main Function, ListenAndServe It has been executed without providing any services.
- Since each connection is processed in a separate goroutine, when the panic is sent in the handler, it will not even affect the main Function, but it will cause the corresponding process to crash.
To provide some help, net/http Package (in) conn.serve Method) has a built-in recovery for each service goroutine. We can see its function through simple examples:
func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello\n") } func doPanic(w http.ResponseWriter, req *http.Request) { panic("oops") } func main() { http.HandleFunc("/hello", hello) http.HandleFunc("/panic", doPanic) http.ListenAndServe(":8090", nil) }
If we run this service, and curl / panic Path, we can see:
$ curl localhost:8090/panic curl: (52) Empty reply from server
And the service will print such information in its own log:
2021/02/16 09:44:31 http: panic serving 127.0.0.1:52908: oops goroutine 8 [running]: net/http.(*conn).serve.func1(0xc00010cbe0) /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 service will remain running and we can continue to access it!
Although this built-in protection mechanism is better than service crash, many developers have found its limitations. This protection mechanism only closes the connection and outputs errors in the log; In general, it is much more useful to return some kind of error response (such as code 500 -- built-in error) and additional details to the client.
After reading this blog post, it should be easy to write middleware to realize this function. Use it as an exercise! I will introduce this use case in a later blog post.
[^ 1]: This version has good reasons to prefer this version over the version using the default mux. The default mux has certain security risks; As a global instance, it can be modified by any package introduced in your project. A malicious package can also use it for evil purposes. [^ 2]: Note: http.HandleFunc and http.HandlerFunc Are different entities with different and interrelated roles.