Core HTTP Handler Patterns

Go's HTTP handlers follow a simple interface: http.Handler requires a ServeHTTP method. This minimal interface enables powerful composition patterns that form the backbone of most Go web applications.

package main

import (
    "fmt"
    "net/http"
)

// Basic handler function
func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page")
}

// Handler that returns JSON
func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"message": "Hello from API"}`)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", homeHandler)
    mux.HandleFunc("/api", apiHandler)
    
    http.ListenAndServe(":8080", mux)
}

Middleware Implementation

Middleware in Go is implemented as functions that wrap handlers, providing cross-cutting concerns without cluttering business logic. The most effective approach uses a decorator pattern that composes handlers cleanly.

package main

import (
    "log"
    "net/http"
    "time"
)

// Middleware function type
type Middleware func(http.HandlerFunc) http.HandlerFunc

// Logging middleware
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next(w, r)
        log.Printf("Request completed in %v", time.Since(start))
    }
}

// Authentication middleware
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Simple token check
        token := r.Header.Get("Authorization")
        if token != "Bearer secret123" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}

// Composing multiple middlewares
func withMiddlewares(handler http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
    for _, middleware := range middlewares {
        handler = middleware(handler)
    }
    return handler
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is a protected resource")
}

func main() {
    mux := http.NewServeMux()
    
    // Apply middleware chain
    protected := withMiddlewares(
        protectedHandler,
        loggingMiddleware,
        authMiddleware,
    )
    
    mux.HandleFunc("/protected", protected)
    http.ListenAndServe(":8080", mux)
}

Request Context and Validation

Go's context package provides a clean way to manage request-scoped values and timeouts, essential for building robust web services.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Custom context keys
type contextKey string

const (
    userIDKey contextKey = "userID"
    requestIDKey contextKey = "requestID"
)

// Context middleware
func contextMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Create context with request ID
        ctx := context.WithValue(r.Context(), requestIDKey, generateRequestID())
        
        // Add user ID if authenticated
        if userID := r.Header.Get("X-User-ID"); userID != "" {
            ctx = context.WithValue(ctx, userIDKey, userID)
        }
        
        next(w, r.WithContext(ctx))
    }
}

// Request validation middleware
func validationMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Validate required headers
        if r.Header.Get("Content-Type") != "application/json" {
            http.Error(w, "Content-Type must be application/json", http.StatusBadRequest)
            return
        }
        
        // Validate request body for POST requests
        if r.Method == "POST" && r.ContentLength == 0 {
            http.Error(w, "Request body is required", http.StatusBadRequest)
            return
        }
        
        next(w, r)
    }
}

// Helper function to extract values from context
func getUserID(r *http.Request) string {
    userID, ok := r.Context().Value(userIDKey).(string)
    if !ok {
        return ""
    }
    return userID
}

func generateRequestID() string {
    return fmt.Sprintf("%d", time.Now().UnixNano())
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    fmt.Fprintf(w, "User ID: %s", userID)
}

func main() {
    mux := http.NewServeMux()
    
    // Chain middleware
    api := withMiddlewares(
        apiHandler,
        contextMiddleware,
        validationMiddleware,
    )
    
    mux.HandleFunc("/api", api)
    http.ListenAndServe(":8080", mux)
}

Performance Optimization Techniques

Effective Go web services require attention to performance patterns. The following examples demonstrate key optimization strategies.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

// Response writer pool for efficient JSON serialization
var responseBufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

// Efficient JSON response handler
func efficientJSONHandler(w http.ResponseWriter, r *http.Request) {
    buffer := responseBufferPool.Get().(*bytes.Buffer)
    defer responseBufferPool.Put(buffer)
    buffer.Reset()
    
    // Prepare JSON response
    data := map[string]interface{}{
        "timestamp": time.Now().Unix(),
        "message":   "Efficient response",
        "status":    "success",
    }
    
    if err := json.NewEncoder(buffer).Encode(data); err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    buffer.WriteTo(w)
}

// Caching middleware
func cachingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    cache := make(map[string]string)
    cacheMutex := sync.RWMutex{}
    
    return func(w http.ResponseWriter, r *http.Request) {
        cacheKey := r.URL.String()
        
        cacheMutex.RLock()
        if cached, exists := cache[cacheKey]; exists {
            cacheMutex.RUnlock()
            w.Header().Set("X-Cache", "HIT")
            fmt.Fprint(w, cached)
            return
        }
        cacheMutex.RUnlock()
        
        // Capture response
        recorder := &responseRecorder{ResponseWriter: w, statusCode: 200}
        next(recorder, r)
        
        // Cache successful responses
        if recorder.statusCode == http.StatusOK {
            cacheMutex.Lock()
            cache[cacheKey] = recorder.body.String()
            cacheMutex.Unlock()
        }
    }
}

// Response recorder for capturing output
type responseRecorder struct {
    http.ResponseWriter
    statusCode int
    body       *bytes.Buffer
}

func (r *responseRecorder) WriteHeader(code int) {
    r.statusCode = code
    r.ResponseWriter.WriteHeader(code)
}

func (r *responseRecorder) Write(b []byte) (int, error) {
    if r.body == nil {
        r.body = new(bytes.Buffer)
    }
    r.body.Write(b)
    return r.ResponseWriter.Write(b)
}

func main() {
    mux := http.NewServeMux()
    
    // Apply caching middleware
    cached := withMiddlewares(
        efficientJSONHandler,
        cachingMiddleware,
    )
    
    mux.HandleFunc("/api", cached)
    http.ListenAndServe(":8080", mux)
}

Comparison of HTTP Handler Approaches

ApproachProsConsBest For
Simple HandlersEasy to understand, minimal overheadNo built-in compositionSimple static sites
Middleware ChainClean separation of concernsSlightly more complexMedium complexity services
Handler CompositionFlexible and reusableRequires careful designComplex applications
HTTP Server with Custom RouterFull control over routingMore boilerplate codeHigh-performance APIs

Best Practices Summary

  1. Use context for request-scoped data - Avoid global variables and pass values through context
  2. Implement middleware composition - Keep cross-cutting concerns separate from business logic
  3. Pool resources for performance - Use sync.Pool for frequently allocated objects
  4. Validate requests early - Check headers and body before processing
  5. Log appropriately - Include request IDs and timing information for debugging
  6. Handle errors gracefully - Return proper HTTP status codes and meaningful messages

Learn more with useful resources