A Go daemon exits its accept loop on any error from net.Listener.Accept. What breaks?
answer
- not every failure is the server's failure
- one lost client should not end the process
- there is a sentinel for a deliberate close
- retrying with no pause spins a core
- the deprecated predicate is Temporary
basics
~20 sOne transient failure kills the whole server: the loop returns and the process stops accepting connections. Accept errors need triage — return only when the listener was deliberately closed, and otherwise log, pause briefly and continue.
solid answer
~40 sAccept can fail for reasons that say nothing about the server's health — a peer that vanished before the accept, a momentary resource shortage — and none of those should end the service. The triage has two branches. If the listener was closed on purpose, Accept returns an error satisfying `errors.Is(err, net.ErrClosed)`, and returning is exactly right: that is your shutdown signal. Anything else is logged and retried, with a short sleep before continuing so that an error which recurs instantly does not spin a core; the standard library's own HTTP server backs off with a growing delay. Do not branch on `net.Error.Temporary`, deprecated precisely because "temporary" was never well defined, and do not match on error strings. Close the listener from your shutdown path so the loop unwinds through the ErrClosed branch.
code
go · 12 linesfor {
conn, err := ln.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return nil // Close was called; this is shutdown
}
log.Printf("accept: %v", err)
time.Sleep(10 * time.Millisecond) // never spin
continue
}
go handle(conn)
}go deeper
Know that Accept can fail without the server being broken, and that the loop should keep going unless the listener was closed on purpose. Log the error rather than swallowing it.
Explain the two branches concretely: errors.Is against net.ErrClosed for shutdown, log-and-continue otherwise, and why the string comparison people write instead is fragile.
An interviewer expects the backoff and the reason for it — an instantly recurring error turns the loop into a spin — plus knowing Temporary is deprecated and that closing the listener is the only way to unblock a parked Accept.
Own the operational contract of the daemon: what counts as a fatal condition worth exiting on so a supervisor restarts it, what is degraded-but-serving, and what the accept path must emit so the on-call engineer can tell the two apart.
## Why the naive loop is wrong ```go for { conn, err := ln.Accept() if err != nil { return err // every failure is fatal } go handle(conn) } ``` This reads like careful error handling and behaves like a single point of failure. `Accept` sits at the boundary between your process and everything outside it, and errors there are not all the same kind of event: - **The listener was closed.** Your own shutdown path did it. Returning is correct. - **A per-connection failure.** The peer connected and then disappeared before you accepted it, or the connection was reset during the handshake. Nothing about the server is broken; one client is gone. - **A resource shortage.** The process is momentarily out of descriptors. Every *existing* connection is still fine and the situation usually clears. - **A genuinely permanent failure.** Rare in practice on a bound listener. A loop that returns on all four turns a lost client into an outage. The daemon exits, the port goes away, and if the process is supervised it flaps. ## The triage that works ```go for { conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return nil // deliberate shutdown } log.Printf("accept: %v", err) time.Sleep(10 * time.Millisecond) continue } go handle(conn) } ``` Three decisions are encoded there. **`net.ErrClosed` is the shutdown signal.** Since Go 1.16 the error returned by operations on a closed listener or connection wraps this sentinel, so `errors.Is` is the supported test. Before that, code compared `err.Error()` against the string `"use of closed network connection"` — you still see it, and it is now an anti-pattern: an unexported error's text is not API. **The sleep is not superstition.** An error that recurs immediately — the descriptor limit is the usual one — makes the loop a spin loop that consumes a whole core while logging thousands of identical lines a second. Even 5–10 ms of pause turns that into a survivable degraded state. The standard library's `http.Server.Serve` does the same thing more carefully, starting at a small delay and doubling it up to a ceiling, then resetting after a successful accept. Copying that shape is reasonable for a hand-rolled server. **Don't use `Temporary()`.** The `net.Error` interface has `Timeout() bool` and `Temporary() bool`, and `Temporary` is documented as deprecated: which errors count as temporary was never specified consistently, and code that branches on it is making a decision on a coin flip. `Timeout()` is still meaningful, but a listener with no deadline set does not produce timeouts. ## Shutting the loop down deliberately Because `Accept` blocks, you cannot poll a flag between iterations — the goroutine is parked inside the call. The way out is to close the listener from elsewhere: ```go go func() { <-ctx.Done() ln.Close() // unblocks Accept with net.ErrClosed }() ``` Closing the listener stops new connections; it does **not** touch connections already accepted, which keep running until their handlers finish. That separation is usually what you want for graceful shutdown: stop taking work, drain what you have. If you are running `http.Server` over your own listener via `Serve(ln)`, the equivalents are `Shutdown(ctx)` — stop accepting, then wait for in-flight requests — and the sentinel `http.ErrServerClosed`, which `Serve` returns afterwards and which you should not treat as a failure. ## What an interviewer is listening for The distinction between fatal and transient, named concretely rather than as a principle; `errors.Is(err, net.ErrClosed)` rather than a string compare; the backoff and *why* it exists; and awareness that `Temporary` is deprecated. Bonus points for saying that closing the listener is the only way to unblock a parked `Accept`, and that doing so leaves established connections alone.
- How do you stop an accept loop that is blocked inside Accept?Close the listener from another goroutine. `Accept` is parked in a blocking call, so no flag or channel check inside the loop can be reached; `ln.Close()` unblocks it with an error satisfying `errors.Is(err, net.ErrClosed)`, which the loop treats as its exit signal. Closing the listener stops new connections but leaves already-accepted ones running until their handlers finish.
- Why not branch on net.Error's Temporary method to decide whether to continue?`Temporary` is deprecated: which errors report true was never defined consistently across platforms and error types, so a branch on it is unreliable in both directions. The supported test is `errors.Is(err, net.ErrClosed)` for a deliberate close; everything else is treated as retryable with backoff. `Timeout()` on net.Error remains meaningful, but only where a deadline was actually set.
- What does the standard library's http.Server do on a failing Accept?It does not give up on the first error. `Serve` retries with a delay that starts small and grows to a ceiling, resetting once an accept succeeds, so a repeating failure degrades the server instead of killing it. After `Shutdown` or `Close` it stops and returns the sentinel `http.ErrServerClosed`, which callers are expected to recognise rather than report as a crash.
The reception desk should not close for the day because one visitor turned around and left before being greeted. It closes when the building closes, and that is a different signal entirely.
saying these in an interview costs you the question
- Returns from the accept loop on every Accept error
- Matches the error text use of closed network connection
- Retries instantly with no pause, spinning a core
- Branches on the deprecated net.Error Temporary method
- Believes closing the listener also kills accepted connections
- Tries to stop the loop with a flag checked between iterations