How do you implement middleware in Go's HTTP model?
Quick Answer
Middleware in Go is a function that takes an http.Handler and returns a new http.Handler wrapping it — a common signature is func(http.Handler) http.Handler. Inside the wrapper, you do work before calling next.ServeHTTP(w, r) (logging the request, checking auth), optionally call next.ServeHTTP to continue the chain, and optionally do work after it returns (logging the response time, adding headers). Chaining multiple middleware means wrapping a handler in each one in sequence: loggingMiddleware(authMiddleware(finalHandler)), with the outermost wrapper's "before" code running first and its "after" code running last, like nested function calls.
Detailed Answer
Go has no special middleware syntax. It's just functions wrapping http.Handler values, which is simple enough to implement from scratch with no framework.
The core pattern
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // "after" code runs once this returns
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
An auth middleware that can short-circuit the chain
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValidToken(token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return // never calls next.ServeHTTP — the chain stops here
}
next.ServeHTTP(w, r)
})
}
Not calling next.ServeHTTP is how a middleware rejects a request early, without any special "abort" mechanism needed.
Chaining several middleware together
finalHandler := http.HandlerFunc(getUserHandler)
wrapped := loggingMiddleware(authMiddleware(finalHandler))
http.ListenAndServe(":8080", wrapped)
Execution order: loggingMiddleware's "before" code, then authMiddleware's "before" code, then finalHandler, then authMiddleware's "after" code (if any), then loggingMiddleware's "after" code — nested exactly like the function calls suggest.
A small helper to make chaining more readable
func chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
wrapped := chain(finalHandler, loggingMiddleware, authMiddleware)
This is a common small utility many projects write themselves, rather than pulling in a router library purely for middleware chaining syntax.