HTTP Patterns
Go 1.22+ patterns and project-specific conventions for HTTP handlers.
Go 1.22: ServeMux Method Routing
No more manual method checks. Register with HTTP verb prefix:
go1mux := http.NewServeMux() 2mux.HandleFunc("GET /healthz", healthHandler) 3mux.HandleFunc("GET /_cooked/{path...}", assetHandler) 4mux.HandleFunc("GET /{upstream...}", renderHandler)
Path Parameters
go1func renderHandler(w http.ResponseWriter, r *http.Request) { 2 upstream := r.PathValue("upstream") // NEW in Go 1.22 3 // ... 4}
Special Patterns
| Pattern | Matches |
|---|---|
/posts/{id} | /posts/123 (single segment) |
/files/{path...} | /files/a/b/c (remainder of path) |
/posts/{$} | /posts/ only (not /posts or /posts/x) |
Precedence
More specific wins:
/healthzbeats/{upstream...}GET /posts/{id}beats/posts/{id}
Conflicting patterns panic at registration:
go1mux.HandleFunc("GET /posts/{id}", h1) 2mux.HandleFunc("GET /{resource}/latest", h2) // PANIC - both match /posts/latest
Automatic 405
Unmatched methods return 405 Method Not Allowed with Allow header.
go.mod Requirement
Go 1.22+ patterns require go 1.22 or later in go.mod. Without it, patterns are treated literally (braces aren't wildcards).
Sources: Go Blog: Routing Enhancements, Eli Bendersky
Middleware Pattern
This project uses the standard wrapper pattern:
go1func RequestLogger(next http.Handler) http.Handler { 2 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3 start := time.Now() 4 wrapped := &byteCountingWriter{ResponseWriter: w} 5 next.ServeHTTP(wrapped, r) 6 slog.Info("request", 7 "method", r.Method, 8 "path", r.URL.Path, 9 "status", wrapped.statusCode, 10 "bytes", wrapped.bytes, 11 "total_ms", time.Since(start).Milliseconds(), 12 ) 13 }) 14} 15 16// Composable 17var handler http.Handler = mux 18handler = RequestLogger(handler) 19handler = RecoveryMiddleware(handler) 20 21srv := &http.Server{Handler: handler}
Request Context
Store request-scoped values using typed keys:
go1// Define typed key (prevents collisions) 2type contextKey string 3const loggerKey contextKey = "logger" 4 5// Set in middleware 6ctx := context.WithValue(r.Context(), loggerKey, logger) 7next.ServeHTTP(w, r.WithContext(ctx)) 8 9// Retrieve in handlers 10func Logger(ctx context.Context) *slog.Logger { 11 if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok { 12 return l 13 } 14 return slog.Default() 15}
ResponseWriter Wrapping
Wrap to capture bytes and status:
go1type byteCountingWriter struct { 2 http.ResponseWriter 3 bytes int64 4 statusCode int 5} 6 7func (w *byteCountingWriter) WriteHeader(code int) { 8 w.statusCode = code 9 w.ResponseWriter.WriteHeader(code) 10} 11 12func (w *byteCountingWriter) Write(b []byte) (int, error) { 13 if w.statusCode == 0 { 14 w.statusCode = 200 // Default if WriteHeader not called 15 } 16 n, err := w.ResponseWriter.Write(b) 17 w.bytes += int64(n) 18 return n, err 19}
Graceful Shutdown
Standard Pattern
go1srv := &http.Server{ 2 Addr: ":8080", 3 Handler: mux, 4} 5 6// Start server 7go func() { 8 if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 9 log.Fatal(err) 10 } 11}() 12 13// Wait for signal 14<-ctx.Done() 15 16// Graceful shutdown with timeout 17shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 18defer cancel() 19 20if err := srv.Shutdown(shutdownCtx); err != nil { 21 log.Printf("shutdown error: %v", err) 22}
What Shutdown() Does
- Stops accepting new connections immediately
- Closes idle connections
- Waits for active requests to complete
- Returns when all handlers finish OR context expires
Kubernetes Considerations
Add a health endpoint that fails during shutdown:
go1healthService.MarkShuttingDown() // Called before Shutdown() 2// Readiness probe now returns 503
Sources: VictoriaMetrics, DEV Community
Error Responses
cooked returns HTML error pages, not JSON:
go1func renderError(w http.ResponseWriter, r *http.Request, status int, errType, message string) { 2 w.Header().Set("Content-Type", "text/html; charset=utf-8") 3 w.WriteHeader(status) 4 // Render error template with same theming as content pages 5}
Error pages use the same HTML template with:
- Proper theming (respects user's theme choice)
#cooked-errorelement withdata-error-typeanddata-status-code- Direct link to the upstream URL
Handler Naming
| Pattern | Returns | Example |
|---|---|---|
FeatureHandler(deps) | http.HandlerFunc | RenderHandler(deps) |
FeatureMiddleware(next) | http.Handler | RequestLogger(next) |
What NOT to Do
| Don't | Why |
|---|---|
Check method with if r.Method != "GET" | Use Go 1.22+ method routing |
Call w.Write after handler returns | Causes panic or writes to wrong response |
Use string context keys | Collisions — use typed keys |
Ignore http.ErrServerClosed | It's expected from Shutdown() |
| Release resources before shutdown completes | Handlers may still be using them |