Middleware & Decorator Patterns
Middleware in Go wraps an http.Handler with another http.Handler to add cross-cutting behavior: logging, authentication, recovery, and metrics.
Search across all documentation pages
Middleware in Go wraps an http.Handler with another http.Handler to add cross-cutting behavior: logging, authentication, recovery, and metrics.
It is the practical form of the decorator pattern in a language without inheritance.
Each middleware receives the next handler, returns a new handler, and decides whether to run logic before, after, or around the inner call.
The net/http server invokes the outermost handler; middleware nests inward until the route handler runs.
The same composition model appears in chi, gin, echo, and gRPC interceptors with framework-specific registration helpers.
Quick-reference recipe card - copy-paste ready.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("handled", "path", r.URL.Path, "ms", time.Since(start).Milliseconds())
})
}When to reach for this:
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type ctxKey int
const userKey ctxKey = 1
func withUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, "demo-user")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func greet(w http.ResponseWriter, r *http.Request) {
user, _ := r.Context().Value(userKey).(string)
w.Write([]byte("hello " + user))
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Use(withUser)
r.Get("/greet", greet)
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}What this demonstrates:
r.Use registers middleware for routes registered on that routerhttp.Error before calling nextcontext.ContextRequest -->
RequestID middleware
--> Recoverer middleware
--> withUser middleware
--> greet handler
<-- withUser (after next returns)
<-- Recoverer
<-- RequestID
Response <--next.ServeHTTP unless it fully handles the requestResponseWriter captures status and body size for metricstype statusWriter struct {
http.ResponseWriter
code int
}
func (w *statusWriter) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}http.ResponseWriter to delegate most methodsWriteHeader and Write to observe behaviorhttp.ResponseController (Go 1.20+) for flush/hijack when needed| Framework | Registration | Handler type |
|---|---|---|
| net/http | Manual nesting | http.Handler |
| chi | r.Use(mw) | http.Handler |
| gin | engine.Use(mw) | gin.HandlerFunc |
| echo | e.Use(mw) | echo.MiddlewareFunc |
| gRPC | grpc.ChainUnaryInterceptor | interceptor func |
next.ServeHTTP unless you wrote the full response."user". Fix: private typed key constants per package.middleware.NewWrapResponseWriter from chi or implement optional interfaces.Recoverer outermost; auth before handlers that need identity.main.| Alternative | Use When | Don't Use When |
|---|---|---|
| Inline handler checks | One route needs auth | Many routes share concern |
| HTTP middleware chain | Cross-cutting transport concerns | Core business branching |
| Decorator structs embedding Handler | Custom servers with defaults | Simple static file server |
| Service mesh / gateway auth | Centralized edge policy | Local dev simplicity matters |
Yes in role - gRPC unary/stream interceptors wrap RPC handlers the same way HTTP middleware wraps ServeHTTP.
Use httptest.NewRecorder and httptest.NewRequest; pass a stub next handler that sets a flag when called.
Yes, but read and restore carefully; prefer limiting body size at server level (MaxBytesReader).
Middleware validates credentials and attaches identity to context; services enforce authorization rules on resources.
There is no fixed max - but if order is hard to reason about, group related concerns or document a standard stack in main.
On chi, yes for routes registered on the router; unmatched paths hit NotFound handler after the same Use stack.
Register the route on a sub-router without that middleware, or no-op inside middleware based on path prefix.
Prefer slog with request-scoped attributes (request ID from context) for structured logs.
gin wraps http.Request in Context; middleware still composes but uses gin's API instead of raw handlers.
Yes - wrap the mux: logging(mux) or register per-method handlers wrapped individually.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026