Go HTTP Demystified
Go is a fantastic tool for anyone writing HTTP-centric code. One could even go as far as to claim that Go is a DSL for HTTP development. Yet the http library is often subtle and easy to misunderstand.
The most basic thing to understand about HTTP programming with Go is this function signature:
ServeHTTP(w http.ResponseWriter, r *http.Request)
This appears in three places of interest, which are ultimately unified to provide HTTP serving functionality:
- Interface type http.Handler
- Function type http.HandlerFunc
- Struct type http.ServeMux
Let's dive into what each of these really mean.
1. Interface type http.Handler
If you need to know one thing, it is that any type that defines a method
ServeHTTP(w http.ResponseWriter, r *http.Request)
satisfies the http.Handler interface and therefore can be used to handle and respond to HTTP requests.
How do we know that the http.Handler interface must be satisfied in order to handle HTTP requests? The http.Server type is the primary means of constructing HTTP servers, and its Handler field has type http.Handler.
A sample implementation could be something as simple as:
type T struct { S string } func (t *T) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) io.WriteString(w, "hello " + t.S) }
Type T defines a ServeHTTP method and therefore satisfies the http.Handler interface.
The http library defines two noteworthy concrete types whose implementations satisfy http.Handler: function type http.HandlerFunc and struct type http.ServeMux.
2. Function type http.HandlerFunc
One thing many people find hard to understand about Go is that function types can have methods defined on them just like struct types. In our case this is important because we know that any type that we want to handle HTTP requests must satisfy interface http.Handler by defining a ServeHTTP method. The http.HandlerFunc type provides this for us.
If a function we write can be converted to an http.HandlerFunc, we will satisfy http.Handler, since http.HandlerFunc provides a concrete definition of ServeHTTP. The definition of this method is subtly confusing:
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w, r) }
func MyHandler(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello") }
we could convert that to a HandlerFunc type:
h := http.HandlerFunc(MyHandler)
and now it has a ServeHTTP method which will just call MyHandler to produce an HTTP response. This seems a little contrived until you realize it unifies function types with struct types cleanly for the purposes of handling HTTP requests.
If you want to save a little typing, you can have Go do this "promotion" for you by calling HandleFunc to attach the handler to a ServeMux instance:
3. Struct type http.ServeMux
http.ServeMux is a multiplexer, meaning it can choose one of many registered handlers to satisfy a request based on a path string match. Since http.ServeMux instances can also be assigned to the Handler field of a http.Server instance, it must also satisfy http.Handler and therefore define a ServeHTTP method. In the case of http.ServeMux, this method is more complex; an appropriate handler from those registered by a caller is selected, or a default error response is returned. Since http.ServeMux is not particularly special by design, we can see how easily it might be replaced. As a result, a rich ecosystem of third-party multiplexing libraries have emerged that still utilize the standard http.Server.
We have illustrated attaching handlers to http.ServeMux instances by invoking the associated HandleFunc method. We can also call http.HandleFunc as a function in which case the handler is attached to DefaultServeMux, which is a predefined http.ServeMux instance available as a convenience.
last update 2017-03-23