Overview
net/http is the reason Go has thin routers instead of heavy frameworks: a complete,
production-grade HTTP server and client in the standard library. Its single-method
http.Handler interface is the interoperability point of the whole ecosystem — every
router, middleware, and observability wrapper that honors it composes with every other.
Since Go 1.22 the built-in ServeMux also handles method matching and path wildcards,
covering what most services previously imported a router for.
Key points
- The contract:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }, withhttp.HandlerFuncadapting a plain function to it. Everything else is built from this. - 1.22 ServeMux patterns:
"GET /items/{id}"matches by method;{id}captures a segment (read withr.PathValue("id"));{path...}captures the remainder; a trailing{$}matches exactly/. The most specific registered pattern wins; genuinely ambiguous overlaps panic at registration. - Middleware = decorators:
func(http.Handler) http.Handler, wrapped outermost-first. No framework machinery — just function composition. - Context flows through the request:
r.Context()carries cancellation (client gone, server shutdown) and request-scoped values; derive withr.WithContext(ctx). Keys should be unexported custom types, and values reserved for cross-cutting data (request ID, auth principal) — not a general parameter bag. - Set server timeouts — the zero-value
http.Servernever times anything out. At minimumReadHeaderTimeout(Slowloris defence); usuallyReadTimeout,WriteTimeout,IdleTimeouttoo. - http.Client misuse is the classic footgun: the zero-value client has no timeout, and creating a client (or Transport) per request defeats connection pooling. Build one client, reuse it, and always fully read and close response bodies so connections return to the pool.
- HTTP/2 is automatic over TLS; cleartext h2c needs explicit opt-in (historically via
golang.org/x/net/http2/h2c, on recent releases via the server's protocol configuration). - Graceful shutdown:
srv.Shutdown(ctx)stops accepting, drains in-flight requests, and unblocksListenAndServewithhttp.ErrServerClosed. - Panics don't kill the server: each connection's goroutine recovers, logs, and drops
the connection. Panicking with
http.ErrAbortHandleraborts a response silently — the sanctioned way to bail without a stack trace in the logs. - chi is the idiomatic thin layer when the stdlib mux runs out: same
http.Handlercontract, plus sub-routers, mounting, and a curated middleware set. - html/template covers server-side rendering with context-aware escaping (HTML, attr,
JS, URL) — safe-by-default output, unlike
text/template.
Details
ServeMux precedence
Registration order is irrelevant; specificity decides. GET /items/{id} beats
/items/{id} (method-specific wins), and /items/latest beats /items/{id} (literal
beats wildcard). GET patterns also match HEAD. Two patterns where neither is more
specific — e.g. /a/{x}/c vs /a/b/{y} — conflict and panic at Handle time, which
surfaces route bugs at startup rather than in production traffic.
Server hygiene
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
Per-route deadlines that must outlive WriteTimeout (long polls, streaming) are better
handled with http.TimeoutHandler or context deadlines than by loosening global knobs.
For shutdown, catch SIGTERM, call srv.Shutdown(ctx) with a drain budget, and only then
release dependencies (DB pools, queues).
Client hygiene
http.Transport owns the connection pool: MaxIdleConns, MaxIdleConnsPerHost (default
2 — raise it for high fan-out to one host), TLS session caching. Client.Timeout bounds
the entire exchange including body read; per-attempt budgets belong to
context.WithTimeout on the request. A body that is closed without being drained may
discard the connection instead of pooling it.
chi on top
chi keeps every handler and middleware stdlib-shaped, adding r.Route/r.Mount for
grouped sub-routers and chi/middleware (RequestID, RealIP, Recoverer, Timeout,
Compress). Because nothing wraps the contract, otelhttp, promhttp and any
func(http.Handler) http.Handler slot in unchanged — the property the
correspondence table is about.
Examples
Middleware plus 1.22 mux patterns, no third-party imports:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path,
"dur", time.Since(start))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
json.NewEncoder(w).Encode(map[string]string{"id": id})
})
mux.HandleFunc("GET /static/{path...}", serveStatic)
log.Fatal(http.ListenAndServe(":8080", logging(mux)))
}
Related
- Go web frameworks — the landscape — parent overview; where net/http sits among the frameworks.
- Frameworks × the stdlib contracts — which frameworks honor the Handler contract described here.
- REST in Go — building REST APIs on this stack.
- API design — the design discipline the handlers implement.