
Go: Building Efficient Web Services with HTTP Handlers and Middleware
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
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| Simple Handlers | Easy to understand, minimal overhead | No built-in composition | Simple static sites |
| Middleware Chain | Clean separation of concerns | Slightly more complex | Medium complexity services |
| Handler Composition | Flexible and reusable | Requires careful design | Complex applications |
| HTTP Server with Custom Router | Full control over routing | More boilerplate code | High-performance APIs |
Best Practices Summary
- Use context for request-scoped data - Avoid global variables and pass values through context
- Implement middleware composition - Keep cross-cutting concerns separate from business logic
- Pool resources for performance - Use sync.Pool for frequently allocated objects
- Validate requests early - Check headers and body before processing
- Log appropriately - Include request IDs and timing information for debugging
- Handle errors gracefully - Return proper HTTP status codes and meaningful messages
