A Recap of Request Handling in Go

12th September 2013

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:

ServeHTTP(ResponseWriter, *Request)

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:

$ mkdir handler-example
$ cd handler-example
$ touch app.go
File: app.go
package main

import (
  "log"
  "net/http"
)

func main() {
  mux := http.NewServeMux()

  h := http.RedirectHandler("http://example.org", 307)
  mux.Handle("/foo", h)

  log.Println("Listening...")
  http.ListenAndServe(":3000", mux)
}

Let's step through this quickly:

Go ahead and run the application:

$ go run app
Listening...

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.

Custom Handlers

Let's create a custom handler which responds with the current time in a given time zone, formatted in RFC 1123 format:

type timeHandler struct {
  zone *time.Location
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(th.zone).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

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:

File: app.go
package main

import (
  "log"
  "net/http"
  "time"
)

type timeHandler struct {
  zone *time.Location
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(th.zone).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

func newTimeHandler(name string) *timeHandler {
  return &timeHandler{zone: time.FixedZone(name, 0)}
}

func main() {
  mux := http.NewServeMux()

  mux.Handle("/est", newTimeHandler("EST"))

  log.Println("Listening...")
  http.ListenAndServe(":3000", mux)
}

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:

File: app.go
package main

import (
  "log"
  "net/http"
  "time"
)

func main() {
  mux := http.NewServeMux()

  mux.Handle("/est", http.HandlerFunc(est))

  log.Println("Listening...")
  http.ListenAndServe(":3000", mux)
}

func est(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

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:

File: app.go
...
func main() {
  mux := http.NewServeMux()

  // mux.Handle("/est", http.HandlerFunc(est))
  mux.HandleFunc("/est", est)

  log.Println("Listening...")
  http.ListenAndServe(":3000", mux)
}
...

The DefaultServeMux

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:

File: app.go
...
func main() {
  http.HandleFunc("/est", est)

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}
...

Chaining Handlers

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.

File: app.go
package main

import (
  "log"
  "net/http"
  "time"
)

type logHandler struct {
  next http.Handler
}

func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  log.Printf("%v: %v", r.Method, r.URL.Path)
  lh.next.ServeHTTP(w, r)
}

func newLogHandler(next http.Handler) *logHandler {
  return &logHandler{next}
}

func main() {
  http.Handle("/est", newLogHandler(http.HandlerFunc(est)))

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func est(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

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 lh.next.ServeHTTP(w, r).

So far, so good. We can also set up our intermediary handler as a function:

File: app.go
package main

import (
  "log"
  "net/http"
  "time"
)

func logHandler(next http.Handler) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    log.Printf("%v: %v", r.Method, r.URL.Path)
    next.ServeHTTP(w, r)
  }
}

func main() {
  http.Handle("/est", logHandler(http.HandlerFunc(est)))

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func est(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

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:

File: app.go
package main

import (
  "log"
  "net/http"
  "time"
)

func logHandler(next http.Handler) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    log.Printf("%v: %v", r.Method, r.URL.Path)
    next.ServeHTTP(w, r)
  }
}

func logHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
  return LogHandler(next)
}

func main() {
  http.Handle("/est", logHandlerFunc(est))

  log.Println("Listening...")
  http.ListenAndServe(":3000", nil)
}

func est(w http.ResponseWriter, r *http.Request) {
  tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123)
  w.Write([]byte("The time is: " + tm))
}

If you found this post useful, you might like to subscribe to my RSS feed.

Filed under: golang, tutorial

‚Äč