Routing and Middleware in Go
Routing and middleware are essential components of web applications. While Go’s standard library provides basic routing, understanding how to build effective routing and middleware systems is crucial for scalable web development. This guide covers both standard library approaches and common patterns.
Basic Routing with Standard Library
Simple Route Handling
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// Define routes
http.HandleFunc("/", homeHandler)
http.HandleFunc("/about", aboutHandler)
http.HandleFunc("/contact", contactHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to Home")
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About Us")
}
func contactHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Contact Us")
}
Route Matching with Path Parameters
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
func main() {
http.HandleFunc("/users/", userHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func userHandler(w http.ResponseWriter, r *http.Request) {
// Extract user ID from path
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
userID := parts[2]
fmt.Fprintf(w, "User ID: %s", userID)
}
Method-Based Routing
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/api/users", usersHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprintf(w, "GET: List all users")
case http.MethodPost:
fmt.Fprintf(w, "POST: Create new user")
case http.MethodPut:
fmt.Fprintf(w, "PUT: Update user")
case http.MethodDelete:
fmt.Fprintf(w, "DELETE: Delete user")
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
Middleware Patterns
Basic Middleware
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Middleware function
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Call next handler
next.ServeHTTP(w, r)
// Log after handler completes
duration := time.Since(start)
fmt.Printf("[%s] %s %s - %v\n", r.Method, r.URL.Path, r.RemoteAddr, duration)
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!")
}
func main() {
handler := http.HandlerFunc(helloHandler)
http.Handle("/", loggingMiddleware(handler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Middleware Chain
package main
import (
"fmt"
"log"
"net/http"
)
// Middleware 1: Logging
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Logging: Request received")
next.ServeHTTP(w, r)
fmt.Println("Logging: Response sent")
})
}
// Middleware 2: Authentication
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Println("Auth: Token validated")
next.ServeHTTP(w, r)
})
}
// Middleware 3: Recovery
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Recovery: Panic recovered: %v\n", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!")
}
func main() {
handler := http.HandlerFunc(helloHandler)
// Chain middleware (applied in reverse order)
handler = recoveryMiddleware(handler)
handler = authMiddleware(handler)
handler = loggingMiddleware(handler)
http.Handle("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Advanced Routing
Router Multiplexer
package main
import (
"fmt"
"log"
"net/http"
)
type Router struct {
mux *http.ServeMux
}
func NewRouter() *Router {
return &Router{
mux: http.NewServeMux(),
}
}
func (r *Router) Get(path string, handler http.HandlerFunc) {
r.mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
handler(w, req)
})
}
func (r *Router) Post(path string, handler http.HandlerFunc) {
r.mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
handler(w, req)
})
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}
func main() {
router := NewRouter()
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Home")
})
router.Post("/api/data", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Data received")
})
log.Fatal(http.ListenAndServe(":8080", router))
}
Route Groups with Middleware
package main
import (
"fmt"
"log"
"net/http"
)
type RouteGroup struct {
prefix string
middlewares []func(http.Handler) http.Handler
mux *http.ServeMux
}
func NewRouteGroup(prefix string, mux *http.ServeMux) *RouteGroup {
return &RouteGroup{
prefix: prefix,
middlewares: []func(http.Handler) http.Handler{},
mux: mux,
}
}
func (rg *RouteGroup) Use(middleware func(http.Handler) http.Handler) *RouteGroup {
rg.middlewares = append(rg.middlewares, middleware)
return rg
}
func (rg *RouteGroup) Handle(path string, handler http.HandlerFunc) {
fullPath := rg.prefix + path
// Apply middlewares
h := http.Handler(handler)
for i := len(rg.middlewares) - 1; i >= 0; i-- {
h = rg.middlewares[i](/programming/h)
}
rg.mux.Handle(fullPath, h)
}
func main() {
mux := http.NewServeMux()
// Create API route group with auth middleware
api := NewRouteGroup("/api", mux)
api.Use(authMiddleware)
api.Handle("/users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Users list")
})
log.Fatal(http.ListenAndServe(":8080", mux))
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Auth check")
next.ServeHTTP(w, r)
})
}
Practical Examples
RESTful API with Routing
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var users = map[int]User{
1: {ID: 1, Name: "Alice"},
2: {ID: 2, Name: "Bob"},
}
func main() {
mux := http.NewServeMux()
// Routes
mux.HandleFunc("/api/users", handleUsers)
mux.HandleFunc("/api/users/", handleUserDetail)
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(users)
case http.MethodPost:
var user User
json.NewDecoder(r.Body).Decode(&user)
users[user.ID] = user
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleUserDetail(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extract ID from path
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 4 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
id := parts[3]
switch r.Method {
case http.MethodGet:
// Get user by ID
for _, user := range users {
if fmt.Sprintf("%d", user.ID) == id {
json.NewEncoder(w).Encode(user)
return
}
}
http.Error(w, "User not found", http.StatusNotFound)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
Middleware for Request Context
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
type contextKey string
const requestIDKey contextKey = "requestID"
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := fmt.Sprintf("%d", time.Now().UnixNano())
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
requestID := r.Context().Value(requestIDKey).(string)
fmt.Fprintf(w, "Request ID: %s", requestID)
}
func main() {
handler := http.HandlerFunc(helloHandler)
http.Handle("/", requestIDMiddleware(handler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
Best Practices
โ Good Practices
// Use middleware for cross-cutting concerns
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing
next.ServeHTTP(w, r)
// Post-processing
})
}
// Chain middleware in logical order
handler = recoveryMiddleware(handler)
handler = authMiddleware(handler)
handler = loggingMiddleware(handler)
// Use context for request-scoped values
ctx := context.WithValue(r.Context(), key, value)
r = r.WithContext(ctx)
// Separate concerns
// - Routing: URL matching
// - Middleware: Cross-cutting concerns
// - Handlers: Business logic
โ Anti-Patterns
// Don't mix routing and middleware logic
// Keep them separate
// Don't create middleware that modifies request path
// This breaks routing
// Don't forget to call next handler
// Middleware must call next.ServeHTTP()
// Don't use global state in middleware
// Use context instead
Resources
- Go net/http Package Documentation
- Go context Package Documentation
- HTTP Middleware Patterns
- RESTful API Design
Summary
Effective routing and middleware are essential for web applications:
- Use standard library
http.ServeMuxfor basic routing - Implement middleware as handler wrappers
- Chain middleware in logical order
- Use context for request-scoped values
- Keep routing and middleware concerns separate
- Test middleware independently
With these patterns, you can build scalable and maintainable web applications in Go.
Comments