skip to content

When two http.ServeMux patterns match the same request, how does Go decide which one wins?

level: middleimportance: must knowfreq 55%

answer

  1. not first match, not longest string
  2. compare the sets of requests matched
  3. strict subset wins
  4. no subset means the pair is rejected
  5. the rejection happens at registration

basics

~20 s

The more specific pattern wins: the one whose set of matching requests is a strict subset of the other's. Registration order is irrelevant. If neither pattern is a subset of the other they conflict, and registering the second one panics.

solid answer

~50 s

Go's `ServeMux` resolves overlaps by **specificity**, not by order. Pattern P1 beats P2 when P1 matches a strict subset of the requests P2 matches — P2 matches everything P1 does, and more. So `"GET /items/new"` beats `"GET /items/{id}"` for the path `/items/new`, and `"/images/thumbnails/"` beats `"/images/"` for anything under the thumbnails subtree, no matter which was registered first. When neither pattern's request set contains the other's, the patterns **conflict**: `"GET /"` and `"/index.html"` both match a GET for `/index.html`, but each also matches requests the other does not. Registering both makes `mux.Handle` or `mux.HandleFunc` panic, so a conflict is a startup crash you find in a test, not a mysterious 404 in production. One backwards-compatibility exception: if two otherwise-conflicting patterns differ in that one carries a host and the other does not, the host pattern wins instead of panicking.

code

go · 7 lines
go
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", showItem)
mux.HandleFunc("GET /items/new", newItemForm) // more specific for this one path
mux.HandleFunc("GET /items/{id}/tags", itemTags)

// GET /items/new -> newItemForm, whatever order these lines appear in
// GET /items/42  -> showItem

go deeper

for a junior

Remember the headline: Go's standard mux does not take the first matching route. The narrower pattern serves the request no matter when it was registered.

for a middle

Explain specificity as a strict-subset relation between the request sets two patterns match, and name the consequence when neither is a subset: the registration call panics.

for a senior

Demonstrate that a legal overlap can still change behaviour, so adding a literal route beside a wildcard route needs a test pinning which pattern serves which path, and that a conflicting pattern is a boot failure worth catching in CI.

for a principal

Own the consequence for a codebase: order-independent routing removes a class of merge hazard, but a panic on a conflicting pattern is an availability event, so make building the real mux part of the test suite everyone inherits.

## The rule in one sentence If two or more registered patterns match a request, the **most specific** one takes precedence; if no pattern is most specific, the patterns conflict and registering them panics. ## What "more specific" actually means Specificity is defined on **request sets**, not on string length and not on how many wildcards a pattern has. Think of each pattern as the set of all requests it matches. Pattern P1 is more specific than P2 when P1's set is a strict subset of P2's: every request P1 matches is also matched by P2, and P2 matches at least one request P1 does not. Worked through: - `"/items/new"` versus `"/items/{id}"`. The literal matches exactly one path. The wildcard matches that path *and* `/items/42`, `/items/anything`. Strict subset, so the literal is more specific and serves `/items/new`. The wildcard still serves every other id. - `"/images/thumbnails/"` versus `"/images/"`. Everything under the thumbnails subtree is also under the images subtree, but not the reverse. The thumbnails pattern is more specific. - `"GET /items/{id}"` versus `"/items/{id}"`. The method-constrained pattern matches a subset (GET and HEAD only), so it wins for GET requests while the unconstrained pattern keeps POST, PUT and the rest. Notice what is **not** the rule. It is not longest-string-wins: `"GET /"` is shorter than `"/index.html"` and neither wins. It is not literals-always-beat-wildcards as a standalone axiom — that falls out of the subset rule when the literal is otherwise identical, but not when the two patterns disagree in different segments. And it is emphatically not first-match: several widely used routers walk their route list in registration order and take the first hit, so an engineer arriving from one of those will reach for reordering as a fix and find it does nothing. ## Conflicts When neither pattern's request set contains the other, there is no defensible winner, so the mux refuses to choose. The documentation's own example is `"GET /"` and `"/index.html"`: both match a GET for `/index.html`; the first also matches every other GET; the second also matches a POST for `/index.html`. Neither set is a subset of the other. Registering both panics. A subtler conflicting pair is `"/items/{id}/tags"` and `"/items/a/{x}"`: both match `/items/a/tags`, but the first also matches `/items/b/tags` and the second also matches `/items/a/colours`. Wildcards that cross each other in different positions are the usual source of accidental conflicts. The panic happens **at registration**, inside `Handle` or `HandleFunc`, with a message naming both patterns. That is a deliberate design choice: a routing ambiguity becomes a process that will not start, which a smoke test or a `TestMain` that builds the real mux catches immediately. It also means a conflicting route added by a merge takes the service down at boot rather than silently stealing traffic — worth saying in an interview, because it is a genuine availability consideration and the argument for building the production mux inside a unit test. ## The one exception For backwards compatibility, if two patterns would otherwise conflict and exactly one of them has a host, the pattern with the host takes precedence rather than the pair being rejected. This preserves the older behaviour where a host-qualified registration was understood to be narrower. It is the only place where the subset rule is overridden, and it is worth naming because it is the kind of detail a follow-up question probes. ## Why order-independence is a feature With first-match routing, the correctness of your routing table depends on the sequence of lines in a file. Two developers adding routes in the same release can reorder each other's matches through a merge, and a broad catch-all placed early makes everything after it unreachable without any error. Go's rule removes that entire class of bug: you can register routes in any order, group them however reads best, and split registration across functions or packages. The price is that overlaps are resolved by a rule you must internalise, and that an ambiguous pair is fatal rather than arbitrary. ## What to do about it in practice Because an overlap can be legal *and* still change which handler serves an existing path, treat the routing table as behaviour under test. Build the real mux in a test and assert, for a table of representative paths, which pattern each one matches. Add a row whenever you add a route. That converts both failure modes — a silent shadow and a startup panic — into a red test on the pull request that introduced them.

  • Does changing the order of the registration calls change which pattern serves a request?
    No. `ServeMux` compares the patterns themselves, so the outcome is identical however you order, group or split the registrations. That is the main behavioural difference from routers that walk a list and take the first hit, and it is why reordering is never the fix for a route that reaches the wrong handler.
  • What happens at startup when two conflicting patterns are registered?
    `Handle` and `HandleFunc` panic, naming both patterns. The process fails to start rather than serving an arbitrary choice. Build the production mux inside a test so the panic surfaces in CI; otherwise the first evidence is a crash loop on deploy.
  • How does a host in the pattern affect precedence?
    A pattern with a host matches only requests for that host, with the port stripped, so it is normally narrower on its own terms. There is also one backwards-compatibility exception: if two patterns would otherwise conflict and only one carries a host, the host pattern takes precedence instead of the pair being rejected.

It is a dictionary of addresses, not a stack of filters. The mux hands the request to the entry that describes it most narrowly, regardless of what order the entries were written down — and if two entries describe it equally well, it refuses to guess.

saying these in an interview costs you the question

  • Says the first registered pattern wins
  • Says the longest pattern string always wins
  • Thinks a conflict is resolved silently at request time
  • Believes reordering registrations can fix a mismatch
  • Assumes any overlap between two patterns is illegal