Processing HTTP requests with Go is primarily about two things: ServeMuxes and Handlers.
A ServeMux is essentially a HTTP request router (or multiplexor). It compares incoming requests against a list of predefined URL paths, and calls the associated handler for the path whenever a match is found.
Handlers are responsible for writing response headers and bodies. Almost any object can be a handler, so long as it satisfies the Handler interface. In lay terms, that simply means it must have a
ServeHTTP method with the following signature:
Go's HTTP package ships with a few functions to generate common handlers, such as FileServer, NotFoundHandler and RedirectHandler. Let's begin with a simple but contrived example:
Let's step through this quickly:
- In the
mainfunction we use the NewServeMux function to create an empty ServeMux.
- We then use the RedirectHandler function to create a new handler, which temporarily redirects all requests it receives to
- Next we use the Handle function to register this with our new ServeMux, so it acts as the handler for all incoming requests with the URL path
- Finally we create a new server and start listening for incoming requests with the ListenAndServe function, passing in our ServeMux for it to match requests against.
Go ahead and run the application:
And visit localhost:3000/foo in your browser. You should find your request gets successfully redirected.
The eagle-eyed of you might have noticed something interesting: The signature for the ListenAndServe function is
ListenAndServe(addr string, handler Handler), but we passed a ServeMux as the second parameter.
We were able to do this because ServeMux has a ServeHTTP method, meaning that it satisfies the Handler interface.
For me it simplifies things to think of a ServeMux as just being a special kind of handler, which instead of providing a response itself passes the request on to a second handler. This isn't as much of a leap as it first sounds – chaining of handlers is fairly commonplace in Go, and we'll take a look at an example further down the page.
Let's create a custom handler which responds with the current time in a given time zone, formatted in RFC 1123 format:
The exact code here isn't too important.
All that really matters is that we have an object (in this case it's a
timeHandler struct), and we've implemented a
ServeHTTP method on it which returns a HTTP response. That's all we need to make a handler.
Let's embed the
timeHandler in a concrete example:
Run the application and visit localhost:3000/est in your browser. You should get the current Eastern Standard Time as a response.
Functions as Handlers
Alternatively, Go's HTTP package provides a convenient way to turn normal functions into handlers, thanks to the HandlerFunc type.
You can convert any function with the signature
func(ResponseWriter, *Request) into a HandlerFunc object. Every HandlerFunc has a
ServeHTTP method built in, which simply calls the original function. If that sounds confusing, try taking a look at the Go source code.
It's a roundabout but very succinct way of turning a function into something that satisfies the Handler interface.
Let's reproduce the time zone application using this approach:
In fact, converting a function to a HandlerFunc and adding it to a ServeMux like this is so common that Go provides a shortcut: the HandleFunc method.
We'll swap our example to use the shortcut instead:
You've probably seen the DefaultServeMux mentioned in lots of places, from the simplest Hello World examples to the Go source code.
It took me a long time to realise there is nothing special about this. The DefaultServeMux is just a plain ol' ServeMux like we've already been using, which gets instantiated by default when the HTTP package is used. Here's the relevant line from the Go source:
var DefaultServeMux = NewServeMux()
The HTTP package provides a couple of shortcuts for working with the DefaultServeMux: http.Handle and http.HandleFunc. These do exactly the same as their namesake functions we've already looked at, with the difference that they add handlers to the DefaultServeMux instead of a manually created one.
Additionally, ListenAndServe will fall back to using the DefaultServeMux if no other handler is provided.
Changing our time zone application to use the DefaultServeMux instead is easy:
Joining handlers together can be a nice way to share common functionality or create 'layers' in your application. Let's adapt our time zone application to include an intermediary
logHandler which records the method and path of requests.
You can probably see what's going on easily enough.
When creating the
logHandler we pass the final handler as a parameter and store it in the
logHandler struct. After the logger is done recording the request, we simply call the next handler in line with the code
So far, so good. We can also set up our intermediary handler as a function:
This is pretty neat. Our
logHandler function simply returns a closure containing the logic as a HandlerFunc type (remember – converting to a HandlerFunc essentially implements a
ServeHTTP method on the closure, so it can be used like any other handler).
You can see this approach to chaining handlers in some third-party packages like Ghost and Handy.
Lastly let's complete things by adding an adapter, so we can chain normal functions to our
logHandler without manually converting them first:
If you found this post useful, you might like to subscribe to my RSS feed.