Go Routing and Middleware
Routing and middleware are essential components of web application architecture in Go. Routing determines how URLs are mapped to handler functions, while middleware provides a way to implement cross-cutting concerns like authentication, logging, and error handling. Understanding routing and middleware patterns is crucial for building maintainable and scalable web applications. This comprehensive guide will teach you everything you need to know about routing and middleware in Go.
Understanding Routing and Middleware
What Is Routing?
Routing in Go determines how HTTP requests are mapped to handler functions based on URL patterns. It provides:
- URL Pattern Matching - Matching URLs to specific handlers
- Dynamic Route Parameters - Extracting parameters from URLs
- HTTP Method Handling - Handling different HTTP methods
- Route Organization - Organizing routes for maintainability
What Is Middleware?
Middleware in Go are functions that sit between the HTTP request and response, providing:
- Cross-cutting Concerns - Authentication, logging, error handling
- Request Processing Pipeline - Processing requests in stages
- Response Modification - Modifying responses before sending
- Code Reusability - Reusable components across routes
Basic Routing Patterns
Simple URL Routing
The http.ServeMux
Type
Go's built-in HTTP request multiplexer for routing.
Route Registration
Registering routes with handler functions.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// Basic routing patterns examples
fmt.Println("Basic routing patterns examples:")
// Simple URL routing
func simpleURLRouting() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to the homepage!")
})
mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "About page: Learn more about our application")
})
mux.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Contact page: Get in touch with us")
})
mux.HandleFunc("/products", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Products page: Browse our products")
})
fmt.Println("Simple URL routing server starting on :8080")
http.ListenAndServe(":8080", mux)
}
// Start simple URL routing server
go simpleURLRouting()
time.Sleep(2 * time.Second)
// HTTP method routing
func httpMethodRouting() {
mux := http.NewServeMux()
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fmt.Fprintf(w, "GET /users: List all users")
case "POST":
fmt.Fprintf(w, "POST /users: Create a new user")
case "PUT":
fmt.Fprintf(w, "PUT /users: Update users")
case "DELETE":
fmt.Fprintf(w, "DELETE /users: Delete users")
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
fmt.Println("HTTP method routing server starting on :8081")
http.ListenAndServe(":8081", mux)
}
// Start HTTP method routing server
go httpMethodRouting()
time.Sleep(2 * time.Second)
// Route with trailing slash
func routeWithTrailingSlash() {
mux := http.NewServeMux()
mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Admin section: %s", r.URL.Path)
})
mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/", http.StatusMovedPermanently)
})
fmt.Println("Route with trailing slash server starting on :8082")
http.ListenAndServe(":8082", mux)
}
// Start route with trailing slash server
go routeWithTrailingSlash()
time.Sleep(2 * time.Second)
// Route grouping
func routeGrouping() {
mux := http.NewServeMux()
// API routes
mux.HandleFunc("/api/v1/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "API v1: %s", r.URL.Path)
})
mux.HandleFunc("/api/v2/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "API v2: %s", r.URL.Path)
})
// Admin routes
mux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Admin: %s", r.URL.Path)
})
// Static routes
mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Static: %s", r.URL.Path)
})
fmt.Println("Route grouping server starting on :8083")
http.ListenAndServe(":8083", mux)
}
// Start route grouping server
go routeGrouping()
time.Sleep(2 * time.Second)
fmt.Println("All basic routing examples completed!")
}
Dynamic Route Parameters
URL Path Parameters
Extracting parameters from URL paths.
Query Parameters
Working with query parameters in routes.
package main
import (
"fmt"
"net/http"
"strings"
"time"
)
func main() {
// Dynamic route parameters examples
fmt.Println("Dynamic route parameters examples:")
// URL path parameters
func urlPathParameters() {
mux := http.NewServeMux()
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/users/")
if path == "" {
fmt.Fprintf(w, "List all users")
return
}
// Extract user ID
parts := strings.Split(path, "/")
if len(parts) > 0 && parts[0] != "" {
userID := parts[0]
fmt.Fprintf(w, "User ID: %s", userID)
// Handle nested routes
if len(parts) > 1 {
switch parts[1] {
case "profile":
fmt.Fprintf(w, " - Profile page")
case "settings":
fmt.Fprintf(w, " - Settings page")
case "posts":
fmt.Fprintf(w, " - Posts page")
default:
fmt.Fprintf(w, " - Unknown page: %s", parts[1])
}
}
}
})
fmt.Println("URL path parameters server starting on :8084")
http.ListenAndServe(":8084", mux)
}
// Start URL path parameters server
go urlPathParameters()
time.Sleep(2 * time.Second)
// Query parameters routing
func queryParametersRouting() {
mux := http.NewServeMux()
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
fmt.Fprintf(w, "Search parameters:\n")
for key, values := range query {
for _, value := range values {
fmt.Fprintf(w, "%s = %s\n", key, value)
}
}
// Get specific parameters
if q := query.Get("q"); q != "" {
fmt.Fprintf(w, "Search query: %s\n", q)
}
if category := query.Get("category"); category != "" {
fmt.Fprintf(w, "Category: %s\n", category)
}
if page := query.Get("page"); page != "" {
fmt.Fprintf(w, "Page: %s\n", page)
}
})
fmt.Println("Query parameters routing server starting on :8085")
http.ListenAndServe(":8085", mux)
}
// Start query parameters routing server
go queryParametersRouting()
time.Sleep(2 * time.Second)
// Dynamic route with validation
func dynamicRouteWithValidation() {
mux := http.NewServeMux()
mux.HandleFunc("/posts/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/posts/")
parts := strings.Split(path, "/")
if len(parts) == 0 || parts[0] == "" {
fmt.Fprintf(w, "List all posts")
return
}
postID := parts[0]
// Validate post ID (simple numeric validation)
if postID == "new" {
fmt.Fprintf(w, "Create new post")
return
}
// Check if post ID is numeric
isValid := true
for _, char := range postID {
if char < '0' || char > '9' {
isValid = false
break
}
}
if !isValid {
http.Error(w, "Invalid post ID", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Post ID: %s", postID)
// Handle nested routes
if len(parts) > 1 {
switch parts[1] {
case "edit":
fmt.Fprintf(w, " - Edit post")
case "delete":
fmt.Fprintf(w, " - Delete post")
case "comments":
fmt.Fprintf(w, " - Comments")
default:
fmt.Fprintf(w, " - Unknown action: %s", parts[1])
}
}
})
fmt.Println("Dynamic route with validation server starting on :8086")
http.ListenAndServe(":8086", mux)
}
// Start dynamic route with validation server
go dynamicRouteWithValidation()
time.Sleep(2 * time.Second)
// Route with multiple parameters
func routeWithMultipleParameters() {
mux := http.NewServeMux()
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
fmt.Fprintf(w, "API root")
return
}
version := parts[0]
resource := parts[1]
fmt.Fprintf(w, "API Version: %s, Resource: %s", version, resource)
if len(parts) > 2 {
id := parts[2]
fmt.Fprintf(w, ", ID: %s", id)
}
if len(parts) > 3 {
action := parts[3]
fmt.Fprintf(w, ", Action: %s", action)
}
})
fmt.Println("Route with multiple parameters server starting on :8087")
http.ListenAndServe(":8087", mux)
}
// Start route with multiple parameters server
go routeWithMultipleParameters()
time.Sleep(2 * time.Second)
fmt.Println("All dynamic route parameters examples completed!")
}
Middleware Patterns
Basic Middleware Implementation
Middleware Function Structure
Understanding how middleware functions work.
Middleware Chaining
Chaining multiple middleware functions together.
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
// Basic middleware implementation examples
fmt.Println("Basic middleware implementation examples:")
// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Authentication middleware
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer valid-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Rate limiting middleware
func rateLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simple rate limiting (in production, use a proper rate limiter)
time.Sleep(100 * time.Millisecond) // Simulate rate limiting
next.ServeHTTP(w, r)
})
}
// Basic middleware server
func basicMiddlewareServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Basic middleware server")
})
mux.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Protected resource")
})
// Apply middleware
handler := loggingMiddleware(corsMiddleware(mux))
fmt.Println("Basic middleware server starting on :8088")
http.ListenAndServe(":8088", handler)
}
// Start basic middleware server
go basicMiddlewareServer()
time.Sleep(2 * time.Second)
// Middleware with conditional application
func conditionalMiddlewareServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Public resource")
})
mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Admin resource")
})
// Apply middleware conditionally
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/admin") {
authMiddleware(mux).ServeHTTP(w, r)
} else {
mux.ServeHTTP(w, r)
}
})
// Apply logging to all requests
finalHandler := loggingMiddleware(handler)
fmt.Println("Conditional middleware server starting on :8089")
http.ListenAndServe(":8089", finalHandler)
}
// Start conditional middleware server
go conditionalMiddlewareServer()
time.Sleep(2 * time.Second)
fmt.Println("All basic middleware examples completed!")
}
Advanced Middleware Patterns
Middleware with Context
Using context for middleware communication.
Middleware with Configuration
Configurable middleware patterns.
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
// Advanced middleware patterns examples
fmt.Println("Advanced middleware patterns examples:")
// Middleware with context
func contextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "requestID", "req-123")
ctx = context.WithValue(ctx, "userID", "user-456")
ctx = context.WithValue(ctx, "timestamp", time.Now())
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// Middleware that uses context
func contextAwareHandler(w http.ResponseWriter, r *http.Request) {
requestID := r.Context().Value("requestID")
userID := r.Context().Value("userID")
timestamp := r.Context().Value("timestamp")
fmt.Fprintf(w, "Request ID: %v\n", requestID)
fmt.Fprintf(w, "User ID: %v\n", userID)
fmt.Fprintf(w, "Timestamp: %v\n", timestamp)
}
// Configurable middleware
func configurableLoggingMiddleware(config map[string]interface{}) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
if config["logRequests"].(bool) {
fmt.Printf("Request: %s %s took %v\n", r.Method, r.URL.Path, time.Since(start))
}
if config["logHeaders"].(bool) {
fmt.Printf("Headers: %v\n", r.Header)
}
})
}
}
// Middleware with error handling
func errorHandlingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Panic recovered: %v\n", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Middleware with response modification
func responseModificationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a custom response writer
rw := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(rw, r)
// Modify response after handler completes
if rw.statusCode >= 400 {
fmt.Printf("Error response: %d for %s %s\n", rw.statusCode, r.Method, r.URL.Path)
}
})
}
// Custom response writer
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Advanced middleware server
func advancedMiddlewareServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Advanced middleware server")
})
mux.HandleFunc("/context", contextAwareHandler)
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("Intentional panic for testing")
})
// Configure logging middleware
loggingConfig := map[string]interface{}{
"logRequests": true,
"logHeaders": false,
}
// Apply middleware chain
handler := errorHandlingMiddleware(
responseModificationMiddleware(
configurableLoggingMiddleware(loggingConfig)(
contextMiddleware(mux),
),
),
)
fmt.Println("Advanced middleware server starting on :8090")
http.ListenAndServe(":8090", handler)
}
// Start advanced middleware server
go advancedMiddlewareServer()
time.Sleep(2 * time.Second)
fmt.Println("All advanced middleware examples completed!")
}
Request Processing Pipeline
Middleware Pipeline Architecture
Pipeline Stages
Understanding the stages of request processing.
Pipeline Optimization
Optimizing middleware pipelines for performance.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// Request processing pipeline examples
fmt.Println("Request processing pipeline examples:")
// Pipeline stage interface
type PipelineStage interface {
Process(w http.ResponseWriter, r *http.Request, next http.Handler)
}
// Logging stage
type LoggingStage struct{}
func (s *LoggingStage) Process(w http.ResponseWriter, r *http.Request, next http.Handler) {
start := time.Now()
fmt.Printf("Request started: %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
fmt.Printf("Request completed: %s %s in %v\n", r.Method, r.URL.Path, time.Since(start))
}
// Authentication stage
type AuthStage struct{}
func (s *AuthStage) Process(w http.ResponseWriter, r *http.Request, next http.Handler) {
token := r.Header.Get("Authorization")
if token != "Bearer valid-token" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Println("Authentication successful")
next.ServeHTTP(w, r)
}
// Rate limiting stage
type RateLimitStage struct{}
func (s *RateLimitStage) Process(w http.ResponseWriter, r *http.Request, next http.Handler) {
// Simulate rate limiting
time.Sleep(50 * time.Millisecond)
fmt.Println("Rate limiting check passed")
next.ServeHTTP(w, r)
}
// Pipeline builder
type PipelineBuilder struct {
stages []PipelineStage
}
func NewPipelineBuilder() *PipelineBuilder {
return &PipelineBuilder{
stages: make([]PipelineStage, 0),
}
}
func (pb *PipelineBuilder) AddStage(stage PipelineStage) *PipelineBuilder {
pb.stages = append(pb.stages, stage)
return pb
}
func (pb *PipelineBuilder) Build(finalHandler http.Handler) http.Handler {
handler := finalHandler
// Build pipeline in reverse order
for i := len(pb.stages) - 1; i >= 0; i-- {
stage := pb.stages[i]
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stage.Process(w, r, handler)
})
}
return handler
}
// Pipeline server
func pipelineServer() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Pipeline server response")
})
mux.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Protected resource accessed")
})
// Build pipeline
pipeline := NewPipelineBuilder().
AddStage(&LoggingStage{}).
AddStage(&RateLimitStage{}).
Build(mux)
// Add authentication for protected routes
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/protected") {
authStage := &AuthStage{}
authStage.Process(w, r, pipeline)
} else {
pipeline.ServeHTTP(w, r)
}
})
fmt.Println("Pipeline server starting on :8091")
http.ListenAndServe(":8091", finalHandler)
}
// Start pipeline server
go pipelineServer()
time.Sleep(2 * time.Second)
fmt.Println("All request processing pipeline examples completed!")
}
What You've Learned
Congratulations! You now have a comprehensive understanding of Go's routing and middleware capabilities:
Basic Routing Patterns
- Understanding URL routing with
http.ServeMux
- Implementing HTTP method routing
- Working with route grouping and organization
- Handling trailing slashes and redirects
Dynamic Route Parameters
- Extracting parameters from URL paths
- Working with query parameters
- Implementing route validation
- Handling multiple route parameters
Middleware Patterns
- Understanding middleware function structure
- Implementing logging, authentication, and CORS middleware
- Chaining middleware functions
- Creating configurable middleware
Advanced Middleware Patterns
- Using context in middleware
- Implementing error handling middleware
- Creating response modification middleware
- Building middleware pipelines
Request Processing Pipeline
- Understanding pipeline architecture
- Building reusable pipeline stages
- Optimizing middleware performance
- Creating flexible pipeline builders
Key Concepts
http.ServeMux
- HTTP request multiplexer for routing- Middleware - Functions that process requests and responses
- Pipeline - Chain of middleware functions
- Context - Request context for middleware communication
- Route parameters - Dynamic values extracted from URLs
Next Steps
You now have a solid foundation in Go's routing and middleware capabilities. In the next section, we'll explore REST API development, which will help you build robust and scalable APIs using the routing and middleware concepts we've learned.
Understanding routing and middleware is crucial for building maintainable web applications. These concepts form the foundation for all the more advanced web development techniques we'll cover in the coming sections.
Ready to learn about REST API development? Let's explore RESTful API design principles and learn how to build comprehensive APIs with proper HTTP methods, status codes, and JSON handling!