Routing with ServeMux
ServeMux matches requests by method and path, with wildcards and most-specific-wins rules that landed in Go 1.22.
mux.HandleFunc("GET /users/{id}", handlerFunc)Routing means deciding which code answers which request. In Go the router is http.ServeMux. You create one with http.NewServeMux, register patterns on it, and give it to the server as its handler. A ServeMux is itself an http.Handler, which is why it can be passed anywhere a handler is wanted.
What a pattern looks like
A pattern can name a method, a path, and parts of the path that vary. "GET /users/{id}" matches a GET request for /users/7 or /users/alice. The braces mark a wildcard: one path segment that may be anything. {rest...} at the end of a pattern matches all remaining segments. A pattern ending in /, such as "/static/", matches everything below it. "/{$}" matches the root path and nothing else. Method prefixes and wildcards were added in Go 1.22; before that version a third-party router was needed for them.
How a winner is chosen
Registration order does not matter. The most specific pattern wins, so /users/new beats /users/{id}. Two patterns that overlap and neither of which is more specific are a mistake, and registering the second one panics at start-up. A GET pattern also answers HEAD requests. If a path matches but the method does not, the mux replies 405 and sets the Allow header for you.
What trips people up
The mux only routes if the server was given it. Calling the package-level http.HandleFunc registers on the shared default router instead, so those routes quietly do not exist on your own mux.
Example
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
// {id} is a wildcard: one segment, any value.
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "item %s", r.PathValue("id"))
})
mux.HandleFunc("POST /items", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
})
// Three requests, answered without a network.
reqs := []*http.Request{
httptest.NewRequest("GET", "/items/7", nil),
httptest.NewRequest("POST", "/items", nil),
httptest.NewRequest("GET", "/nope", nil), // nothing registered
}
for _, r := range reqs {
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, r)
fmt.Printf("%s %s -> %d\n", r.Method, r.URL.Path, rec.Code)
}
}
// Output:
// GET /items/7 -> 200
// POST /items -> 201
// GET /nope -> 404When to use it
- Map a REST resource onto GET, POST, PUT and DELETE handlers without a router library.
- Give an API a /v1 prefix that the inner handlers do not have to know about.
- Return 405 with a correct Allow header for the wrong method, for free.
More examples
What each pattern matches
One place to see every pattern form side by side. Note that "GET /{$}" and "/" can both be registered: {$} is more specific, so the root path goes to home and every other unmatched path goes to notFoundPage. Without {$}, "GET /" would match every GET path rather than only the root, and because it is still more specific than "/", home would also answer everything meant for notFoundPage.
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", home) // only "/"
mux.HandleFunc("GET /users", listUsers) // only "/users"
mux.HandleFunc("GET /users/new", newForm) // beats the wildcard below
mux.HandleFunc("GET /users/{id}", showUser)
mux.HandleFunc("POST /users", createUser) // same path, other method
mux.HandleFunc("GET /files/{path...}", serveFile) // "/files/a/b/c"
mux.HandleFunc("/", notFoundPage) // any method, anything elseMounting one mux inside another
Grouping routes by writing a second mux keeps a versioned or namespaced API readable, and lets one group be wrapped in its own middleware later. http.StripPrefix returns a handler that edits the request path and then calls the handler it wraps, which is why the inner patterns start at /users rather than /v1/users.
api := http.NewServeMux()
api.HandleFunc("GET /users/{id}", showUser)
api.HandleFunc("GET /orders/{id}", showOrder)
root := http.NewServeMux()
// StripPrefix removes "/v1" from the path before api sees it,
// so api's patterns do not have to repeat the version.
root.Handle("/v1/", http.StripPrefix("/v1", api))
log.Fatal(http.ListenAndServe(":8080", root))Patterns that conflict panic at start-up
Go reports an ambiguous route table immediately, by panicking during registration, rather than picking a winner at request time. The failure happens at process start-up, so it shows up on the first run and never as an intermittent production mystery. Fix it by making one pattern strictly more specific, or by merging the two into one handler.
mux := http.NewServeMux()
mux.HandleFunc("GET /files/{name}/info", a)
mux.HandleFunc("GET /files/report/{kind}", b)
// Registering the second one panics: "/files/report/info" would
// match both and neither pattern is more specific than the other.
Discussion