Skip to main content
โšก Calmops

Routing and Middleware in Go

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

Summary

Effective routing and middleware are essential for web applications:

  • Use standard library http.ServeMux for 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