Implement Middleware
Problem
Cross-cutting concerns like logging, authentication, and CORS require repetitive code across handlers. Without middleware, this logic gets duplicated.
This guide shows middleware patterns in Go.
Solution
1. Basic Middleware Pattern
type Middleware func(http.Handler) http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("Completed in %v", time.Since(start))
})
}
func main() {
handler := loggingMiddleware(http.HandlerFunc(helloHandler))
http.ListenAndServe(":8080", handler)
}2. Chaining Middleware
func chainMiddleware(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
handler := chainMiddleware(
http.HandlerFunc(helloHandler),
loggingMiddleware,
authMiddleware,
corsMiddleware,
)
http.ListenAndServe(":8080", handler)
}3. Context-Based Middleware
type contextKey string
const userIDKey contextKey = "userID"
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
userID := validateToken(token)
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(userIDKey).(int)
fmt.Fprintf(w, "User ID: %d", userID)
}How It Works
Middleware Execution Chain
Middleware wraps handlers in layers, creating an onion-like execution flow:
Request → Middleware 1 (before) → Middleware 2 (before) → Handler
Middleware 1 (after) ← Middleware 2 (after) ← HandlerEach middleware:
- Receives request from previous layer
- Executes pre-processing logic
- Calls
next.ServeHTTP(w, r)to proceed - Executes post-processing logic after handler returns
- Returns to previous layer
Function Signature Pattern
Standard middleware signature:
func(http.Handler) http.HandlerThis pattern allows:
- Wrapping: Each middleware wraps the next handler
- Composition: Multiple middleware can be chained
- Reusability: Same middleware works with any handler
- Type Safety: Compiler enforces correct usage
Context Propagation
context.Context passes request-scoped values through middleware chain:
- Original Context:
r.Context()starts with background context - Add Values:
context.WithValue(ctx, key, value)creates new context - Update Request:
r.WithContext(ctx)attaches context to request - Read Values:
r.Context().Value(key)retrieves values downstream
Important: Context keys should be unexported types to avoid collisions.
Early Exit Pattern
Middleware can stop the chain:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isAuthorized(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return // Don't call next.ServeHTTP
}
next.ServeHTTP(w, r)
})
}When middleware returns early, subsequent middleware and handler don’t execute.
Response Writer Wrapping
Capture response details by wrapping http.ResponseWriter:
type responseWriter struct {
http.ResponseWriter
status int
size int
}
func (rw *responseWriter) WriteHeader(status int) {
rw.status = status
rw.ResponseWriter.WriteHeader(status)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
size, err := rw.ResponseWriter.Write(b)
rw.size += size
return size, err
}This allows middleware to inspect response status and size.
Variations
1. Middleware with Configuration
Create configurable middleware using closures:
func rateLimitMiddleware(requestsPerSecond int) Middleware {
limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), 1)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
// Usage:
handler := rateLimitMiddleware(100)(http.HandlerFunc(myHandler))Trade-offs: Flexible configuration but adds function nesting.
2. Method-Based Middleware
Apply middleware only to specific HTTP methods:
func methodMiddleware(method string, mw Middleware) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == method {
mw(next).ServeHTTP(w, r)
} else {
next.ServeHTTP(w, r)
}
})
}
}
// Usage: Apply CSRF only to POST requests
handler := methodMiddleware("POST", csrfMiddleware)(http.HandlerFunc(createHandler))Trade-offs: Conditional application but increases complexity.
3. Router-Level Middleware (with gorilla/mux)
Apply middleware to specific routes or route groups:
import "github.com/gorilla/mux"
func main() {
r := mux.NewRouter()
// Global middleware
r.Use(loggingMiddleware)
// API subrouter with auth
api := r.PathPrefix("/api").Subrouter()
api.Use(authMiddleware)
api.HandleFunc("/users", usersHandler)
// Public routes (no auth)
r.HandleFunc("/health", healthHandler)
http.ListenAndServe(":8080", r)
}Trade-offs: Fine-grained control but requires routing library.
4. Panic Recovery Middleware
Catch panics and return error responses:
func recoveryMiddleware(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: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}Trade-offs: Prevents crashes but may hide bugs.
5. Metrics and Monitoring Middleware
Collect performance metrics:
import "github.com/prometheus/client_golang/prometheus"
var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
},
[]string{"method", "path", "status"},
)
)
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
duration := time.Since(start).Seconds()
requestDuration.WithLabelValues(
r.Method,
r.URL.Path,
fmt.Sprintf("%d", rw.status),
).Observe(duration)
})
}Trade-offs: Valuable observability but adds overhead.
Common Pitfalls
1. Forgetting to Call next.ServeHTTP
Problem: Middleware doesn’t call the next handler:
// Bad: Handler never executes
func badMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Request received")
// Forgot to call next.ServeHTTP(w, r)
})
}Solution: Always call next.ServeHTTP unless intentionally stopping the chain:
// Good: Always call next
func goodMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Before handler")
next.ServeHTTP(w, r)
log.Println("After handler")
})
}2. Wrong Middleware Order
Problem: Middleware applied in incorrect order causes issues:
// Bad: Logging after auth means failed auth not logged
handler := chainMiddleware(
http.HandlerFunc(myHandler),
authMiddleware, // Runs second
loggingMiddleware, // Runs first
)Solution: Order middleware carefully (execution is reverse of declaration):
// Good: Logging wraps auth, so all requests logged
handler := chainMiddleware(
http.HandlerFunc(myHandler),
loggingMiddleware, // Runs first (outermost)
authMiddleware, // Runs second (inner)
)
// Execution order:
// 1. loggingMiddleware (before)
// 2. authMiddleware (before)
// 3. myHandler
// 4. authMiddleware (after)
// 5. loggingMiddleware (after)3. Modifying Request After Calling next.ServeHTTP
Problem: Attempting to modify response after handler executed:
// Bad: WriteHeader after next.ServeHTTP has no effect
func badMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
// Too late! Headers already sent
w.Header().Set("X-Custom", "value")
w.WriteHeader(http.StatusOK)
})
}Solution: Modify response before calling next.ServeHTTP:
// Good: Set headers before next handler
func goodMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Custom", "value") // Before next
next.ServeHTTP(w, r)
})
}4. Using String Keys for Context Values
Problem: String context keys can collide:
// Bad: String keys can conflict
func badAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "userID", 123)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Another package might use same key!
func otherMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "userID", "different")
// ...
})
}Solution: Use unexported type for context keys:
// Good: Unexported type prevents collisions
type contextKey string
const userIDKey contextKey = "userID"
func goodAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), userIDKey, 123)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Retrieve with same key type
userID := r.Context().Value(userIDKey).(int)5. Not Handling Type Assertions
Problem: Context value type assertions panic on wrong type:
// Bad: Panics if value is nil or wrong type
userID := r.Context().Value(userIDKey).(int)Solution: Check type assertion:
// Good: Safe type assertion
userID, ok := r.Context().Value(userIDKey).(int)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}6. Blocking Middleware
Problem: Slow middleware blocks all requests:
// Bad: Slow operation blocks handler
func badMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Slow external call blocks request
time.Sleep(5 * time.Second)
next.ServeHTTP(w, r)
})
}Solution: Use goroutines for async operations or optimize:
// Good: Async logging
func goodLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
// Log asynchronously
go func() {
duration := time.Since(start)
log.Printf("%s %s - %v", r.Method, r.URL.Path, duration)
}()
})
}Related Patterns
Related Tutorial: See Intermediate Tutorial - Web for middleware fundamentals.
Related How-To: See Build REST APIs for API middleware patterns, Use Context Effectively for context propagation, Graceful Shutdown for cleanup in middleware.
Related Cookbook: See Cookbook recipes “HTTP Middleware”, “Authentication Middleware”, “Logging Middleware” for ready-to-use middleware implementations.