{} The Go Reference

Http · Web · Intermediate

Routing & Middleware

Dispatch and cross-cutting concerns — the Go 1.22 ServeMux (method+path patterns, wildcards, precedence), path values, middleware as Handler-wrapping-Handler, chaining order, and recovery/logging middleware.

Http Intermediate ⏱ 5 min read Complete

🧅 Analogy

Middleware is an onion around your handler. A request travels inward through each layer — logging, authentication, rate-limiting — reaches the core handler, then the response travels back out through the same layers. Each layer is the same shape (Handler), so you can add, remove, or reorder them freely.

Routing with the standard mux

Since Go 1.22, http.ServeMux routes by method and captures path variables — so you rarely need a third-party router:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)      // method + wildcard
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /static/", serveStatic)     // trailing / = subtree
// inside getUser: id := r.PathValue("id")

The matching rules: a request matches the most specific pattern (a fixed path beats a wildcard); a method-specific pattern (GET /x) beats a method-less one (/x); a trailing-slash pattern (/static/) matches the whole subtree; and {name...} captures the rest of the path. A request that matches a path but not its method gets an automatic 405 Method Not Allowed.

Middleware wraps a handler

A middleware takes a Handler and returns a Handler — so they compose by nesting. Run it all in-process with a recorder:

graph LR
REQ["request"] --> L["logging"] --> A["auth"] --> M["mux → handler"]
M --> A2["auth (return)"] --> L2["logging (return)"] --> RES["response"]
middleware.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

// middleware: same shape in, same shape out
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-Request-ID", "req-123")
	next.ServeHTTP(w, r) // call the wrapped handler
})
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "user %s", r.PathValue("id")) // path variable
})

handler := withRequestID(mux) // wrap the whole mux in one line

rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/users/42", nil))

fmt.Println("status:    ", rec.Code)                         // 200
fmt.Println("request-id:", rec.Header().Get("X-Request-ID")) // req-123
fmt.Println("body:      ", rec.Body.String())                // user 42
}

Chaining middleware, and the order

Real apps stack several. A tiny chain helper folds a slice so the first-listed runs first (outermost). This playground records the actual entry/exit order so you can see the onion unwind:

chain.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
)

type Middleware func(http.Handler) http.Handler

// chain applies middlewares so mw[0] is the OUTERMOST (runs first).
func chain(h http.Handler, mw ...Middleware) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
	h = mw[i](h)
}
return h
}

func trace(name string, log *[]string) Middleware {
return func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		*log = append(*log, "→"+name)
		next.ServeHTTP(w, r)
		*log = append(*log, "←"+name)
	})
}
}

func main() {
var log []string
final := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	log = append(log, "handler")
})

h := chain(final, trace("log", &log), trace("auth", &log))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

fmt.Println(strings.Join(log, " "))
// →log →auth handler ←auth ←log
}

A recovery middleware

The most important middleware to write first: one that recovers from a panic so a single bad handler can’t crash the server. It defers a recover(), logs, and returns a 500:

recovery.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	defer func() {
		if rec := recover(); rec != nil {
			http.Error(w, "internal server error", http.StatusInternalServerError)
			fmt.Println("recovered panic:", rec) // in real code: log it
		}
	}()
	next.ServeHTTP(w, r)
})
}

func main() {
boom := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	panic("handler blew up")
})

rec := httptest.NewRecorder()
recoverer(boom).ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))

fmt.Println("status:", rec.Code) // 500 — server survives
}

Reference

ConcernHow
Route by method+pathmux.HandleFunc("GET /users/{id}", h)
Path variabler.PathValue("id")
Subtree routetrailing / ("/static/")
Catch-all wildcard{rest...}
Middleware typefunc(http.Handler) http.Handler
Apply oneh = mw(h)
Apply many (first = outer)chain(h, a, b, c)
Must-have middlewarerecovery, request logging, auth

🐹 Middleware is just composition — keep it boring

The whole pattern is func(http.Handler) http.Handler. Resist a framework until the standard mux genuinely can’t express your routes. Common middlewares: panic recovery (write this first), request logging, auth, CORS, gzip, and rate limiting. Put cross-cutting concerns here and keep handlers focused on one job. Mind the order — recovery should be outermost so it catches panics from everything inside it.

See also

Next: securing it over the wire — TLS & HTTPS.

Check your understanding

Score: 0 / 5

1. What is middleware in Go's net/http?

Middleware is just `func(next http.Handler) http.Handler`. It returns a handler that does something (log, auth, set a header), then calls next.ServeHTTP — composable because the input and output are the same interface.

2. In Go 1.22+, what does the pattern "GET /users/{id}" give you?

Since Go 1.22 the standard ServeMux understands method prefixes and {name} wildcards. It matches the method and captures the segment, available via r.PathValue("id") — no third-party router needed for most apps.

3. In what order do wrapped middlewares run for `log(auth(mux))`?

The outermost wrapper sees the request first. log's handler runs, calls auth's handler, which calls the mux; responses unwind back out through auth then log — like nested function calls, because that's exactly what they are.

4. A chain helper applies middlewares left-to-right as chain(h, A, B, C). For A to be the OUTERMOST layer, how must it wrap?

To make the first-listed middleware outermost (run first), fold the slice from the end: for i := len(mw)-1; i>=0; i-- { h = mw[i](h) }. Then A is applied last, ending up as the outermost wrapper that sees the request first.

5. Why is a recovery middleware important?

Each request runs in its own goroutine, and an unrecovered panic there terminates the process. A recovery middleware defers a recover(), logs the panic, and writes http.StatusInternalServerError — so one bad request can't take the server down. (net/http's own server recovers per-request, but explicit middleware lets you log and shape the response.)

Comments

Sign in with GitHub to join the discussion.