AlgoMaster Logo

ServeMux Enhanced Routing (Go 1.22+)

Medium Priority11 min readUpdated June 6, 2026

Before Go 1.22, the standard library's http.ServeMux could only match URL paths, not HTTP methods, and it had no concept of path parameters like {id}. Most teams used a third-party router (gorilla/mux, chi, httprouter) to get those features. Go 1.22 fixed that gap. The built-in ServeMux now understands method-prefixed patterns, named wildcards, and trailing-slash wildcards, which is enough to build a real REST API without a router dependency.

What Changed in Go 1.22

The old ServeMux matched only on the path. mux.HandleFunc("/products/", h) ran h for every method, every nested path, and gave you no way to read an ID from the URL. Reading /products/42 meant slicing the path string yourself.

Since Go 1.22, a pattern can carry three new things: an HTTP method, a host, and one or more wildcards. The full shape of a pattern is:

Each piece is optional except the path. Some examples:

PatternMatches
/productsAny method, exact path /products
GET /productsOnly GET to /products
POST /productsOnly POST to /products
GET /products/{id}GET to /products/anything, captures id
DELETE /products/{id}DELETE to /products/anything, captures id
GET /static/{path...}GET to /static/ plus any suffix, captures full suffix
GET shop.example.com/productsGET to /products on that host only
/admin/Any method, subtree match for /admin/ and everything under it

The method must be uppercase and followed by a single space. The host, if present, comes before the leading slash of the path. Wildcards are written as {name} for a single path segment and {name...} for "the rest of the path".

Method-Prefixed Patterns

Methods used to require an if r.Method != "GET" { ... } check inside every handler. With Go 1.22, you register one handler per method, and the mux dispatches for you.

When a path is registered for at least one method and a request comes in with a different method, ServeMux automatically returns 405 Method Not Allowed and sets the Allow header listing the registered methods. Previously the same request would have hit a 404, which is misleading because the resource exists; the method just isn't supported.

A pattern without a method prefix still matches every method, so this remains legal:

GET, POST, HEAD, anything goes. Use the method-less form for endpoints that genuinely don't care, like health checks or version stamps.

Path Wildcards and r.PathValue

A {name} segment matches exactly one path segment and binds it to a variable you read with r.PathValue("name").

PathValue returns a string. If you need an integer, parse it with strconv.Atoi and handle the error:

A single pattern can carry multiple wildcards. A nested resource like a review on a product reads naturally:

A {name} wildcard refuses to match an empty segment or to span a slash. GET /products/{id} will not match /products/ or /products/42/edit. That keeps the routes precise without you having to validate the shape of the ID.

Trailing-Slash Wildcards: {path...}

{name...} is a different beast. It captures everything from that point to the end of the path, slashes included. It must be the last element of the pattern.

{path...} matches the empty string too, which is why /static/ itself reaches the handler. If you want a different response for the bare /static/ path, register a separate pattern for it. The more specific one wins.

This wildcard form is the modern replacement for the old subtree-match trick where you'd write mux.Handle("/static/", http.StripPrefix("/static/", fs)) and then slice the path yourself. The named form makes the intent explicit and gives you a real variable.

Exact Match vs Subtree Match

The trailing slash has always carried meaning in ServeMux, and it still does in 1.22. Two flavors of registration coexist:

RegistrationBehavior
/cartExact match. Only the path /cart (and the redirect from /cart/ if no /cart/ is registered).
/cart/Subtree match. Matches /cart/, /cart/x, /cart/x/y/z, and so on.
/cart/{path...}Subtree match with a captured variable for the suffix.

The subtree form is useful for things like proxying or serving a file tree. For a real REST API, prefer the exact form plus method prefixes; it's more explicit and the routes read like a contract.

A subtle behavior: if you register only /cart/ (subtree) and a request arrives for /cart, ServeMux issues a 301 Moved Permanently redirect to /cart/. Register both forms if you want both to be served directly without redirect.

The decision flow above is what ServeMux runs on every request. First it asks whether any registered pattern matches the method-path pair. If yes, it picks the most specific one (more on precedence next) and runs the handler. If no pattern matches the full method-path pair but at least one matches the path with a different method, it returns 405. Otherwise, 404.

Pattern Precedence and Conflicts

When two patterns could match the same request, ServeMux picks the more specific one. The rule, in plain language: a literal segment beats a wildcard, and a single-segment wildcard beats a trailing {...} wildcard. Combine that with method-scoped patterns and you can build a routing table that does what you'd expect.

A common case is a list endpoint with a "featured" route:

A request to GET /products/featured matches both patterns. ServeMux picks the literal one, so featuredHandler runs. A request to GET /products/42 only matches the second pattern, so productByIDHandler runs and r.PathValue("id") is "42".

If two patterns are truly equivalent in specificity, the registration panics so you find the conflict at startup, not at request time.

That's a feature, not a bug. A silently shadowed route is the kind of issue that survives until production. The panic forces you to resolve it now.

The same applies across overlap that isn't literal duplication. Patterns where neither is strictly more specific than the other also panic. For instance, GET /a/{x}/c and GET /a/b/{y} both could match GET /a/b/c, and neither is strictly more specific, so registering both panics.

Read the diagram right-to-left: each incoming request finds the most specific pattern it matches. The literal /products/featured beats the wildcard. The two-wildcard pattern is more specific than the one-wildcard pattern for the deeper path. The {path...} form matches everything under /static/.

Host Matching

A pattern can include a host before the path. The mux then matches both the Host header and the path.

Both endpoints register /products, but one only fires for requests with Host: shop.example.com and the other for Host: admin.example.com. A request with any other host falls through to a host-less pattern if one exists, or returns 404.

Host patterns are useful when one binary serves multiple virtual hosts. For a single-host service, you almost never need them. If you do use a host pattern, remember that the host comes from the Host header (or the URL's authority for HTTP/2), and it's case-insensitive.

Putting It Together: A Product Catalog API

A small REST API for a product catalog uses every feature in this lesson. Five routes cover list, get, create, update, and delete.

Each handler reads its ID from r.PathValue("id") rather than slicing r.URL.Path. The 405 response on the unsupported PATCH is automatic from the mux, including the Allow header. The mutex is there because map access from multiple goroutines isn't safe, and every request runs on its own goroutine.

This is roughly fifty lines of routing logic for a five-endpoint REST API with no external dependencies. The 1.22 changes are what make that possible.

Quiz

ServeMux Routing Quiz

10 quizzes