A middleware in Go is a function that wraps an HTTP handler to add common behavior before or after handling a request.
The request passes through several layers before reaching the final handler. Each layer can measure time, validate a token, write logs, or cut off the request if something is wrong.
The good news is that in Go we don’t need anything special. With net/http, a middleware is simply a function that receives and returns an http.Handler.
Recalling handlers
An HTTP handler in Go implements this interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}And a regular function can become a handler with http.HandlerFunc.
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
}
handler := http.HandlerFunc(hello)Structure of a middleware
The typical approach is this:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before the handler
next.ServeHTTP(w, r)
// After the handler
})
}next represents the next step in the chain. It can be another middleware or the final handler.
Logging middleware
Let’s look at a practical example: logging method, route, and duration.
package main
import (
"log"
"net/http"
"time"
)
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
}()
next.ServeHTTP(w, r)
})
}And we use it like this:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
})
server := logging(mux)
log.Fatal(http.ListenAndServe(":8080", server))
}All routes in the mux now go through the middleware.
Authentication middleware
A middleware can also cut off the request before reaching the handler.
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}Notice the return. If the token is invalid, we respond with the error and do not call next.
This token example is only for educational purposes. In a real application we would validate JWT, sessions, API keys, or the appropriate mechanism, and we would never hardcode secrets.
Chaining middlewares
We can wrap multiple layers:
server := logging(auth(mux))The request will first pass through logging, then auth, and finally mux.
If you want it to read better, you can create a small chain function.
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}Usage:
server := chain(mux, logging, auth)Order matters. Logging all requests is not the same as logging only authorized ones.
Recovery middleware
The net/http server already recovers from handler panics, logs a trace, and cuts off the response or connection. Having your own middleware allows you to control the logging and attempt to send a 500 response.
import "runtime/debug"
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic handling %s: %v\n%s", r.URL.Path, err, debug.Stack())
http.Error(w, "Internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}This does not turn panic into a normal error handling tool. If the handler has already sent headers, we won’t be able to change the status code to 500. Furthermore, recovering from a panic does not fix the bug: we must log it and correct its cause.