HTTP Servers, Handlers & ServeMux Patterns
net/http routes requests through ServeMux to Handler implementations.
Search across all documentation pages
net/http routes requests through ServeMux to Handler implementations.
Go 1.22 modernized pattern matching so method-aware REST routes work without a third-party router.
An HTTP server listens on a port, parses requests, and dispatches them to handlers.
http.Server wraps the listener with timeout and TLS knobs.
http.ServeMux maps host, method, and path patterns to handlers.
Handlers implement ServeHTTP or use the HandlerFunc adapter.
Middleware wraps handlers to add logging, auth, and metrics before your business logic runs.
Quick-reference recipe card - copy-paste ready.
mux := http.NewServeMux()
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /api/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, `{"id":%q}`, id)
})
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
log.Fatal(srv.ListenAndServe())When to reach for this:
package main
import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
)
type user struct {
ID string `json:"id"`
Name string `json:"name"`
}
var users = map[string]user{
"1": {ID: "1", Name: "Ada"},
"2": {ID: "2", Name: "Lin"},
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("POST /api/users", createUser)
api := stripPrefix("/api", mux)
srv := &http.Server{
Addr: ":8080",
Handler: logging(api),
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
u, ok := users[id]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(u)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var in user
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
in.ID = "3"
users[in.ID] = in
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(in)
}
func stripPrefix(prefix string, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, prefix)
if p == "" {
p = "/"
}
r2 := r.Clone(r.Context())
r2.URL.Path = p
h.ServeHTTP(w, r2)
})
}
func logging(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 %s", r.Method, r.URL.Path, time.Since(start))
})
}What this demonstrates:
ServeMux.r.PathValue for {id} segments./api.http.Server timeout fields.ServeMux matches the longest applicable pattern considering method and host rules.ServeHTTP runs on the connection's goroutine unless you spawn work yourself.ResponseWriter buffers headers until the first Write or explicit WriteHeader.| Pattern | Matches |
|---|---|
/files/ | Any method, paths under /files/ |
GET /users/{id} | GET only, captures id |
GET /items/{id}/ | GET with trailing slash segment |
{$} | Catch-all for unmatched paths (use carefully) |
// Avoid registering on the global mux in libraries:
// http.HandleFunc(...) // bad in reusable packages
mux := http.NewServeMux() // good: local, testable
// Host-scoped routes:
mux.HandleFunc("GET example.com/admin/", adminHandler)| Type | Use when |
|---|---|
http.HandlerFunc | Simple function handlers |
Struct with ServeHTTP | Handlers needing deps (DB, config) |
http.FileServer | Static assets |
http.StripPrefix + inner mux | Mount sub-apps |
http.DefaultServeMux in libraries - Third-party imports can register conflicting routes. Fix: Pass http.NewServeMux() from main.ReadHeaderTimeout - Slow clients can hold connections without sending headers. Fix: Set it on every production http.Server.Write locks status to 200. Fix: Call WriteHeader or http.Error before body bytes.r.PathValue requires Go 1.22+ patterns. Fix: Upgrade Go or use a router like chi./users and /users/ are different patterns. Fix: Register both or redirect consistently.| Alternative | Use When | Don't Use When |
|---|---|---|
| chi | Want route groups and middleware stacks on stdlib handlers | You need zero dependencies |
| gorilla/mux | Regex routes and host constraints are central | Go 1.22 patterns already suffice |
| gin / echo | Binding, validation, and large middleware ecosystems | You want minimal magic and stdlib types |
Raw ServeMux | Small API, few routes, stdlib only | Complex regex or multi-tenant host routing |
Patterns can include HTTP methods and {name} wildcards.
r.PathValue reads captured segments.
Older prefix-only matching still works for unqualified patterns.
Register separate patterns per method.
Unregistered method/path pairs return 404; explicit method checks inside a catch-all handler can return 405.
Create multiple http.Server values sharing the same handler, each with its own ListenAndServe goroutine.
Use mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
Set cache headers in wrapping middleware if needed.
Yes when handlers need dependencies like *sql.DB.
Store deps on the struct and implement ServeHTTP once per route type.
Patterns may include a host prefix like api.example.com/.
Useful when one process serves multiple virtual hosts.
It wraps a handler and limits request body bytes read.
Pair with http.MaxBytesReader for JSON endpoints accepting POST bodies.
Use httptest.NewRequest with method and path, then mux.ServeHTTP(rec, req).
Assert status and body with httptest.ResponseRecorder.
Yes - each request typically runs on its own goroutine.
Protect shared handler state with mutexes or confine state to request scope.
Run a second listener on :80 that returns http.Redirect to TLS URLs, or terminate TLS at the load balancer.
Not with plain ServeMux.
Use separate ports, cmux, or a gateway that routes by protocol.
Handle registers an http.Handler.
HandleFunc registers a function via the HandlerFunc adapter.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026