Nesting Middleware in Go

I recently, finally, figured out how to properly nest middleware in Go.

Maybe this isn’t news to everyone else, but I couldn’t quite figure out how to apply middleware to more than a single handler in Go. Well over the weekend it magically clicked.

🔗 Middleware

If you are unaware, middleware is some code that typically runs some code before or after a web request. There are other kinds of middleware but they are irrelevant to this post. Here’s a slightly simplified bit of middleware I like and use:

type logline struct {
	Time       string
	Duration   float64
	URL        string
	StatusCode int
}

func Log(logger io.Writer) func(http.Handler) http.Handler {
	e := json.NewEncoder(logger)

	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()

			lw := &loggingResponseWriter{ResponseWriter: w}
			defer func() {
				e.Encode(logline{
					Time:       start.Format(time.RFC3339Nano),
					Duration:   time.Now().Sub(start).Seconds(),
					URL:        r.URL.String(),
					StatusCode: lw.statusCode,
				})
			}()
			h.ServeHTTP(lw, r)
		})
	}
}

type loggingResponseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (lrw *loggingResponseWriter) WriteHeader(code int) {
	lrw.statusCode = code
	lrw.ResponseWriter.WriteHeader(code)
}

The above middleware can log all requests as json to some io.Writer. To install it you just do something like this:

http.Handle("/foo", Log(os.Stdout)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... })))

The above is pretty noisy and contributes to why I hadn’t figured this out sooner, so I’ll unpack this one part at a time.

First off, http.Handle takes an http.Handler, which is simply an interface that has a ServeHTTP method. One confusing part above is that we used http.HandlerFunc, not any thing even mentioning ServeHTTP. Well http.HandlerFunc is just a type that adds the ServeHTTP method. That’s not so bad.

Ok so next is that while we added the above to the /foo endpoint, it’s totally unclear how we could apply that middleware to the entire webserver. Joke’s on us because we used a global and that makes this way harder than it has to be. Here’s how you apply it to the whole web server:

mux := http.NewServeMux()

mux.Handle("/foo", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ... }))

h := Log(os.Stdout)(mux)

In the above code, h is an opaque http.Handler wrapped around the whole ServeMux. You can wrap another ServeMux around this if you wanted to only apply the middleware to part of the tree of urls. Hope this is helpful!


If you are interested in learning Go, this is my recommendation:

(The following includes affiliate links.)

If you don’t already know Go, you should definitely check out The Go Programming Language. It’s not just a great Go book but a great programming book in general with a generous dollop of concurrency.

Another book to consider learning Go with is Go Programming Blueprints. It has a nearly interactive style where you write code, see it get syntax errors (or whatever,) fix it, and iterate. A useful book that shows that you don’t have to get all of your programs perfectly working on the first compile.

Posted Mon, Jul 8, 2019

If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.