Skip to main content

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!