skip to content

Should your Go service scaffold ban http.DefaultServeMux and ListenAndServe, and how would you enforce that?

level: principalimportance: nice to knowfreq 30%

answer

  1. the technical call is the easy half
  2. shape beats rule
  3. a rule that cries wolf gets disabled
  4. relocate the capability, do not remove it
  5. measure exposure, not compliance

basics

~20 s

Yes for the default mux, but enforce it as a default rather than a prohibition: the scaffold's bootstrap takes an explicit ServeMux and returns a configured http.Server, so no generated service can reach the global mux. Lint only catches drift.

solid answer

~50 s

The technical half is settled: `http.DefaultServeMux` should never be served, because avoiding it costs one line and its failure — a dependency's `init` publishing routes on your public listener — is silent and reaches production. The judgment is the enforcement. A ban is a rule someone must police; the stronger move is to make the alternative the only path, so the scaffold ships a `NewServer(cfg, handler) *http.Server` that every generated service calls and the global mux becomes unreachable by construction. Lint then only catches drift, and it must resolve the call rather than match text: the package function `http.ListenAndServe` is the smell, while the method `srv.ListenAndServe()` is the pattern you are promoting, and a rule flagging both gets suppressed. Scope hard failures to the production deploy path, and verify by probing deployed services for `/debug/pprof/` rather than counting compliant reviews.

code

go · 11 lines
go
type Config struct {
	Addr string `json:"addr"`
}

// NewServer is the only way a generated service builds its server.
func NewServer(cfg Config, h http.Handler) *http.Server {
	return &http.Server{
		Addr:    cfg.Addr,
		Handler: h, // required argument, so Handler is never nil
	}
}

go deeper

for a junior

You will not own this decision, but know why the scaffold you were given creates its own mux and passes it explicitly: it makes the global mux unreachable, so no imported package's routes end up on your public port.

for a middle

Be able to argue the technical half cleanly — what the global mux exposes, what an explicit mux costs — and to spot in review when a service has drifted back to the package-level registration functions.

for a senior

Show that you would change the template rather than the review checklist, and that you would keep profiling available on a private listener instead of deleting it. Know why a naive lint rule on ListenAndServe misfires.

for a principal

Own the whole call: the default, the narrow rule, the scope of the hard failure, the escape hatch with a name on it, who can overrule you and on what evidence, and the outcome measure that tells you the exposure is actually gone.

## Separate the technical call from the organisational one The technical call is easy and you should make it quickly: **a service should never serve `http.DefaultServeMux`.** The cost of avoiding it is a single line — `mux := http.NewServeMux()` — and the cost of not avoiding it is that any package anywhere in the build, including a transitive dependency, can publish routes on your public listener from an `init` function. Asymmetric cost, silent failure, trivial fix. There is nothing to debate. Everything interesting is in the second question: **what does "ban" mean operationally, and what does it cost you to enforce?** ## Make the right thing the default, not the wrong thing forbidden A prohibition asks every engineer to remember a rule at the moment they are least interested in it. A default asks nothing: ```go func NewServer(cfg Config, h http.Handler) *http.Server { return &http.Server{Addr: cfg.Addr, Handler: h} } ``` If the scaffold's generated `main` already calls this, and the generated `routes()` already returns a mux built with `http.NewServeMux`, then the default mux is unreachable in every service that starts from the template. You have converted a policy into a shape. Nobody has to be persuaded, nobody has to be corrected in review, and a new hire who has never heard the rule complies by doing nothing. The scaffold buys something else too: the same constructor is where every server-level setting your organisation cares about lands, so the next fleet-wide change is one edit to the template rather than a campaign. ## Then decide what the rule is actually for A default only covers services that start from the template and never drift. So you still want a check — but its job is now narrow: catch drift, not teach the lesson. That reframing changes how you write it. - **Write it on the call graph, not on text.** `http.ListenAndServe` (the package function) and `http.Handle`/`http.HandleFunc` (which register on the global mux) are the smells. `srv.ListenAndServe()` is a method call on your own server value and is exactly the pattern you are promoting. A grep for `ListenAndServe` flags both. A rule with false positives on the code you are advocating is a rule engineers learn to suppress, and once they are suppressing it they suppress the true positives too. - **Scope the hard failure.** Blocking a merge is a real cost, and it is worth paying on services in the production deploy path. On a one-file internal tool, a scratch repository, or an example in documentation, the shorthand is genuinely fine, and failing those builds spends credibility you will want later. - **Give it an escape hatch with a name on it.** An annotated exemption that records who accepted the risk is better than a rule people route around silently. ## Who can overrule you, and on what This is the part that makes it a leadership question rather than a code-style one. You own the scaffold's default and the check. You do not own the outcome alone. - **The security reviewer can force your hand.** The concrete finding — a deployed service answering 200 on `/debug/pprof/`, exposing goroutine stacks, the process command line, and a CPU profile anyone can trigger — is theirs, and it outranks a stylistic preference. If they escalate, your job is to have already shipped the fix as a default so the remediation is a template bump rather than a fleet-wide code change. - **Service teams can legitimately push back on the mechanism, not the default.** "We need profiling in production" is a real requirement, and answering it with "the rule says no" is the wrong answer. Answer it by mounting `net/http/pprof`'s exported handlers on a second `http.Server` bound to an internal listener, and ship that in the scaffold too. A policy that removes a capability people need gets circumvented; one that relocates it gets adopted. ## Measure the outcome, not the compliance The check that tells you the policy worked is **an external probe of every deployed service for `/debug/pprof/` and `/debug/vars`**, run continuously. Counting pull requests that cited the rule, or confirming the template compiles, measures the process rather than the exposure. Services predate the scaffold, get vendored, get copied from a blog post; only the probe sees all of them. ## What a strong answer sounds like It takes a position (yes on the default mux, and the shorthand follows from it), converts the prohibition into a default so enforcement is cheap, narrows the lint rule so it does not cry wolf on the method call, scopes the hard failure to what is deployed, names who can overrule the decision and on what evidence, and picks an outcome measure rather than a compliance measure. Answers that stop at "ban it, it is bad practice" have solved the easy half.

  • A team says they need profiling endpoints in production and your rule blocks them. What do you ship?
    Relocate rather than refuse. Add a second http.Server in the scaffold, bound to an internal listener, serving net/http/pprof's exported handlers on its own mux. The capability stays, the public listener stays clean, and the decision now has a line of code and an owner. A policy that removes something people genuinely need is a policy that gets routed around.
  • Why is a grep-based lint rule for ListenAndServe a poor enforcement tool here?
    Because the package function http.ListenAndServe and the method call srv.ListenAndServe() share a name and only the receiver distinguishes them, and the method is the pattern you are recommending. A rule that flags correct code teaches engineers to suppress it, and suppressions are indiscriminate. Resolve the call properly, or lint only for http.DefaultServeMux, http.Handle and http.HandleFunc, which have no legitimate use in a service.
  • How do you know the policy actually worked across the fleet?
    Probe deployed services from outside for /debug/pprof/ and /debug/vars, continuously. Template adoption, review compliance and lint pass rates all measure the process; only the probe covers services that predate the scaffold, were vendored, or were copied from somewhere else. Fix the finding, then ask why that service escaped the default.

saying these in an interview costs you the question

  • Answers only the technical half and calls it settled
  • Enforces by review culture rather than by the scaffold's shape
  • Writes a text-match lint rule that flags the method call too
  • Blocks merges fleet-wide including throwaway tools
  • Removes profiling entirely rather than relocating it
  • Measures compliance with the rule instead of the exposure
  • Ignores that the security reviewer can escalate over this