Go Middleware — HTTP Middleware Patterns for Logging, Auth, Rate Limiting, and CORS
In this tutorial, you will learn about Go Middleware. We cover key concepts, practical examples, and best practices to help you master this topic.
Go HTTP middleware chains functions that wrap http.Handler for cross-cutting concerns like logging, authentication, rate limiting, and CORS.
What You'll Learn
- Middleware pattern and chaining
- Common middleware implementations
- Context-based middleware
- Third-party middleware
Why It Matters
Middleware separates concerns. Docker uses middleware for auth. Kubernetes uses middleware for audit logging. DodaZIP uses middleware for request tracing.
Real-World Use
Authentication, request logging, rate limiting, CORS, metrics collection, request validation, tracing.
flowchart LR
A["Middleware"] --> B["Logging"]
A --> C["Auth"]
A --> D["Rate Limit"]
A --> E["CORS"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Middleware Pattern
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before handler
next.ServeHTTP(w, r)
// After handler
})
}
Logging Middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
duration := time.Since(start)
log.Printf("%s %s %d %v",
r.Method, r.URL.Path, wrapped.statusCode, duration)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
Authentication Middleware
func AuthMiddleware(validTokens map[string]string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
token = strings.TrimPrefix(token, "Bearer ")
userID, ok := validTokens[token]
if !ok {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Rate Limiting Middleware
func RateLimitMiddleware(requests int, per time.Duration) func(http.Handler) http.Handler {
type client struct {
count int
resetAt time.Time
}
var mu sync.Mutex
clients := make(map[string]*client)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
mu.Lock()
c, exists := clients[ip]
if !exists || time.Now().After(c.resetAt) {
c = &client{resetAt: time.Now().Add(per)}
clients[ip] = c
}
c.count++
mu.Unlock()
if c.count > requests {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
CORS Middleware
func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
origins := make(map[string]bool)
for _, o := range allowedOrigins { origins[o] = true }
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origins[origin] || origins["*"] {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
Middleware Chain
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
wrapped := Chain(handler,
LoggingMiddleware,
AuthMiddleware(validTokens),
RateLimitMiddleware(100, time.Minute),
CORSMiddleware([]string{"*"}),
)
http.ListenAndServe(":8080", wrapped)
}
Common Mistakes
1. Not Passing Context
// Use r.WithContext to pass values to handlers
ctx := context.WithValue(r.Context(), key, val)
next.ServeHTTP(w, r.WithContext(ctx))
2. Deadlock in Middleware
Rate limiting middleware using maps without mutex protection causes data races.
3. Swallowing Panics
Always use recovery middleware as the outermost layer. Never let panics escape.
4. Writing Response in Wrong Order
Set headers before writing body. Status code defaults to 200 if not set.
5. Blocking in Middleware
Don't block the Goroutine in middleware. Use async patterns for heavy work.
Practice Questions
1. What is the middleware pattern? A function that takes http.Handler and returns http.Handler. It wraps the handler with pre/post processing.
2. How do you pass data from middleware to handler? Use context.WithValue to store data in request context. Handler retrieves with r.Context().Value(key).
3. What is the purpose of CORS middleware? Controls which origins can access the API. Sets Access-Control-* headers.
4. How do you limit requests per IP? Track request counts in a map keyed by IP. Check count before passing to next handler.
Challenge: Build a middleware that adds request ID to every response header.
Solution
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), "request_id", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
FAQ
{{< faq question="What is the order of middleware execution?" >}} Outermost middleware executes first (pre), then inner middleware, then handler. Response flows back through middleware in reverse order. {{< /faq >}}
{{< faq question="Can middleware abort the request?" >}} Yes. Call http.Error or w.WriteHeader to send response. Don't call next.ServeHTTP to abort. {{< /faq >}}
{{< faq question="How do I make middleware configurable?" >}}
Return a closure from a Factory function: func MyMiddleware(config Config) func(http.Handler) http.Handler.
{{< /faq >}}
{{< faq question="What is the difference between Gin and net/http middleware?" >}} Same pattern. Gin uses gin.HandlerFunc and c.Next(). net/http uses http.Handler chaining. {{< /faq >}}
{{< faq question="How do I measure request duration?" >}} Record time.Now() before next.ServeHTTP, calculate duration after. log.Printf or record metric. {{< /faq >}}
Try It Yourself
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Request: %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
http.Handle("/", logging(http.HandlerFunc(hello)))
http.ListenAndServe(":8080", nil)
}
Expected output — logs each request and responds with "Hello, World!".
What's Next
Now that you understand middleware, explore testing HTTP handlers.
| Topic | Description | Link |
|---|---|---|
| Go Testing HTTP | Testing HTTP handlers | {{< ref "42-testing-http" >}} |
| Go Web Frameworks | Web frameworks | {{< ref "40-web-frameworks" >}} |
| Go Deployment | Deploying Go apps | {{< ref "43-deployment" >}} |