Go by Example: Custom Middleware
Go 1.23
Understand the Middleware pattern in Go web development. This example shows how to create custom middleware to wrap HTTP handlers, enabling cross-cutting concerns like logging, authentication, and error handling to be applied consistently.
Code
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 the next handler
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
// Wrap the mux with the middleware
wrappedMux := loggingMiddleware(mux)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", wrappedMux)
}Explanation
Middleware allows you to wrap HTTP handlers to execute code before and after the main handler logic. This is ideal for cross-cutting concerns like logging, authentication, and panic recovery.
The Middleware Pattern:
- Signature:
func(http.Handler) http.Handler. This allows middleware to be chained together (A wraps B wraps C). - Control Flow: Code before
next.ServeHTTPruns on the request way in. Code after it runs on the response way out. - Context: Middleware often adds data to the request context (e.g., User ID) for downstream handlers to use.
Code Breakdown
11
The middleware signature: it takes an http.Handler (the 'next' handler) and returns a new http.Handler. This is the standard pattern for Go middleware.
12
We return an http.HandlerFunc, which is a function type that implements the http.Handler interface. This allows us to define the handler logic inline using an anonymous function.
13
Code here runs BEFORE the request is handled. We record the start time to calculate latency later.
16
next.ServeHTTP(w, r) passes control to the next handler in the chain. This is crucial; without it, the request would stop here.
18
Code here runs AFTER the request has been handled. We log the method, path, and duration. This is how we can measure response times.
27
http.NewServeMux creates a new request multiplexer. In a real app, you might use a router like chi or gorilla/mux, but ServeMux is sufficient for simple cases.
31
We wrap the entire mux with our loggingMiddleware. This ensures that EVERY request handled by this mux goes through the logger first.

