Handlers & Routing
Everything in net/http sits on top of one interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
That's it — a handler is anything with a ServeHTTP(w, r) method. Writing a
type that satisfies this interface by hand is rare, though, because the
standard library gives you http.HandlerFunc, an adapter that turns any
plain function with the signature func(http.ResponseWriter, *http.Request)
into a Handler. In practice you'll almost always write ordinary functions
and let the router wrap them for you — which is exactly what
mux.HandleFunc does under the hood.
ResponseWriter is a Writer
http.ResponseWriter does two jobs. First, it's an io.Writer — anything
you already know about writing bytes (from Readers & Writers) transfers
directly: fmt.Fprint(w, "ok") and fmt.Fprintf(w, "Hello, %s!", name)
both work unmodified, because Fprint/Fprintf only need a Writer.
Second, it controls the response's status and headers via w.WriteHeader(code)
and w.Header().Set(key, value). If you never call WriteHeader explicitly,
the first call to Write (which Fprint triggers) sends an implicit 200 OK — that's why the exercise never sets a status for its success cases.
Request carries everything about the call
*http.Request bundles the method (r.Method), the parsed URL
(r.URL, including r.URL.Query() for query parameters), and the body
(r.Body, an io.ReadCloser — read it the same way you'd read any other
reader). For this lesson you only need one more piece: path parameters,
covered next.
ServeMux: the router
http.ServeMux matches an incoming request's method and path against
registered patterns and dispatches to the matching handler. You build one
with http.NewServeMux() and register routes with mux.HandleFunc(pattern, fn).
Since Go 1.22, patterns can include the HTTP method and wildcard segments:
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "item %s", id)
})
A pattern like "GET /items/{id}" only matches GET requests to paths of
that shape — r.PathValue("id") retrieves the segment that matched
{id}. This buys you two things for free that older Go code had to
implement by hand:
- A request to a registered path with the wrong method (e.g.
POST /items/42against aGET-only pattern) gets an automatic405 Method Not Allowed. - A request to a path with no matching pattern at all gets an automatic
404 Not Found.
That's the whole shape of the exercise: GET /health returns a fixed
string, and GET /greet/{name} reads a wildcard segment and echoes it
back:
func NewMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
mux.HandleFunc("GET /greet/{name}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.PathValue("name"))
})
return mux
}
Two handlers, two patterns, no routing library. This is enough for a large share of real services — anything fancier (regex constraints, route groups, middleware chains) is either a small helper on top of this or a third-party router, and you now have the baseline to evaluate whether you need one.
Actually running it
A *http.ServeMux is itself a Handler, so serving it for real is one
line:
http.ListenAndServe(":8080", mux)
ListenAndServe blocks, opens a real network listener, and calls
mux.ServeHTTP for every incoming connection. You won't run that here,
though — the exercise's tests drive NewMux() directly with
net/http/httptest, constructing a request and a response recorder in
memory and calling ServeHTTP on them without opening a socket. That
means your router's logic is fully testable, and fast, before it ever
touches a network.
Practice
1. A Small Router
IntermediateAn HTTP server in Go is a
ServeMux(a router) plus handler functions. Since Go 1.22 the mux understands method + wildcard patterns, so routing rarely needs a third-party library.Implement
NewMux() *http.ServeMuxthat registers two routes:GET /health→ status 200, body exactlyokGET /greet/{name}→ status 200, bodyHello, {name}!(e.g./greet/Ada→Hello, Ada!)
Use
mux.HandleFunc("GET /health", ...)— theGETprefix means aPOSTto the same path gets an automatic405 Method Not Allowed, and an unknown path gets404, both for free. Read the captured segment withr.PathValue("name"), and write the body withfmt.Fprint/fmt.Fprintf(aResponseWriteris just anio.Writer).The hidden tests drive your mux with
httptest— no real network, no ports.package main import ( "net/http" ) // NewMux builds the router. Go 1.22+ ServeMux understands method + wildcard // patterns: "GET /greet/{name}" matches only GET and captures {name}, and a // non-GET request to a GET-only pattern gets an automatic 405. func NewMux() *http.ServeMux { mux := http.NewServeMux() // TODO: register "GET /health" and "GET /greet/{name}" return mux } func main() { http.ListenAndServe(":8080", NewMux()) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.