How it works
The explanation, with examples you can run.
Routing with ServeMux
package main import ( "fmt" "io" "net/http" "net/http/httptest") func main() { mux := http.NewServeMux() mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "item %s\n", r.PathValue("id")) // Go 1.22+ patterns }) srv := httptest.NewServer(mux) // a real listener on 127.0.0.1 defer srv.Close() for _, req := range []struct{ method, path string }{ {"GET", "/items/42"}, {"POST", "/items/42"}, {"GET", "/nope"}, } { r, _ := http.NewRequest(req.method, srv.URL+req.path, nil) resp, err := http.DefaultClient.Do(r) if err != nil { panic(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() fmt.Printf("%s %s -> %d %q\n", req.method, req.path, resp.StatusCode, body) }}Output: GET /items/42 -> 200 "item 42\n", POST /items/42 -> 405 "Method Not Allowed\n" and GET /nope -> 404 "404 page not found\n". Go 1.22 added methods and {name} wildcards to patterns (and {path...} for the rest of a path). When two patterns match, the more specific one wins, and conflicting registrations panic at start-up. For many services this removes the need for a third-party router.
What a handler should know
- It runs concurrently with other handlers. Anything shared, such as a cache map or a counter, needs synchronisation.
r.Context()is cancelled when the client disconnects, when an HTTP/2 stream is reset, or whenServeHTTPreturns. Pass it to database and HTTP calls so abandoned work stops.- Panics are recovered per request and logged, so one bad request doesn't kill the server. Goroutines the handler starts are not protected.
- Always close response bodies on the client side, as above, or connections can't be reused.
Middleware
Because a Handler is just an interface with one method, middleware is a function that takes a handler and returns one: func logging(next http.Handler) http.Handler. It can inspect the request, call next.ServeHTTP(w, r), and look at what happened afterwards. Authentication, request IDs, metrics and panic reporting are all written this way, and r.WithContext lets middleware attach values or a deadline for the handlers below it.
Timeouts and shutdown
A zero http.Server has no timeouts, so a client that sends its headers one byte a minute keeps a goroutine and a file descriptor forever. Configure them:
srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 2 * time.Minute,}For per-handler limits, http.TimeoutHandler wraps a handler, and http.NewResponseController (Go 1.20) can extend a deadline for one streaming response.
On SIGTERM, call srv.Shutdown(ctx): it closes the listeners, closes idle connections, and waits for active requests to finish or for ctx to expire. ListenAndServe then returns http.ErrServerClosed, which is the normal case, not a failure. The client side has the same issue: http.DefaultClient has no overall timeout, so build an http.Client with Timeout or use request contexts.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.