DEV Community

Colin Duggan
Colin Duggan

Posted on

Middleware in Go

Middleware is a powerful idea that allows us to decompose complex systems into layers of abstractions. When we discuss middleware in the context of Go we are referring to the ability of some self-contained code (third-party or other) to hook into a server's request/response processing before or after its handler is invoked. This is a powerful concept which allows us to separate cross-cutting concerns such as:

  • Observability
  • Logging
  • Authentication

Middleware code typically does one thing and then passes the request to the next layer or final processing before returning to the client. This results in code that is more modular, easier to maintain and reusable.

How It Works

HTTP request processing in Go is handled by two things, ServeMux and handlers. The ServeMux, a request multiplexer, simplifies the association between URLs and handlers by matching the URL of incoming requests against predefined patterns and then forwarding to the appropriate handler. Using middleware we can interrupt this flow and instruct our middleware function to act before or after the handler function has executed. We can also dictate whether to apply the middleware to all request or just those matching a particular pattern.

The following snippet represents a typical example of how we would use a ServerMux to associate an incoming request URL with a specific handler.

func main() {
  mux := http.NewServeMux()
  mux.Handle("/users", http.HandlerFunc(usersList))
}
func usersList(w http.ResponseWriter, req *http.Request){
  // fetch users
}
Enter fullscreen mode Exit fullscreen mode

Our handler of type func(w http.ResponseWriter, req *http.Request) doesn't satisfy the http.Handler interface and therefore cannot be passed directly to mux.Handle.

To resolve this issue and satisfy the http.Handler contract we use the http.HandlerFunc(usersList) expression. Note this is not a function call but instead a conversion. The HandlerFunc type has methods and satisfies the http.Handler interface. Its ServeHTTP method calls our underlying function, therefore acting as an adapter. We can now use our usersList function as a http handler now that we are satisfying the http.Handler interface.

// 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)
}
Enter fullscreen mode Exit fullscreen mode

Adding Middleware

Armed with the knowledge of how a typical request handler is constructed we can now quite easily extend our example to include a middleware capability. Once again we are relying on the http.HandlerFunc adapter type to allow us wrap our middleware function so it implements the http.Handler interface. This time we also want the ability to chain handlers together.

func middlewareFunc(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  // Some logic …
  next.ServeHTTP(w, r)
 }
}
Enter fullscreen mode Exit fullscreen mode

The anonymous inner function closes over the next variable allowing us to effectively chain or transfer control from this handler to the next by calling ServeHTTP. Because the inner function has the signature func(rw http.ResponseWriter, r *http.Request) we can convert it to a HandlerFunc type using the http.HandlerFunc adapter.

The signature of our middleware function can be replicated by other middlewares to create arbitrarily long chains. Control can also be stopped at any time by simply issuing a return from the middleware handler instead of calling the next handler.

Top comments (0)