Why do naked returns in a long Go function with named results make bugs easy to miss?
answer
- the statement names nothing
- the meaning lives in the signature
- an early exit ships untouched zeros
- gofmt and the compiler are both content
- fine at six lines, not at two hundred
basics
~10 sA bare return ships whatever the named results hold at that line, and names nothing itself. In a long function a branch that skips an assignment silently returns a plausible zero value nobody chose.
solid answer
~50 sWith named results, a `return` carrying no operands sends back the current values of those variables — so the statement is correct by construction and says nothing about what it returns. In a short function that is fine, because the assignments are all on screen. In a long one the reader has to scroll to the signature for the names and then replay every branch to know the state at each exit, and the compiler will not help: a path that never assigns a result silently returns its zero value. That is how a validator returns `(nil, false)` from an early exit and a CI step prints "config invalid" with an empty list of problems. My review rule is that bare returns are acceptable only where the whole function fits on screen; anywhere longer, spell out `return problems, ok` at each exit and keep the names for their documentation value.
code
go · 9 linesfunc validate(path string) (problems []string, ok bool) {
data, err := os.ReadFile(path)
if err != nil {
return // (nil, false) — reports invalid, shows nothing
}
problems = checkAll(data)
ok = len(problems) == 0
return
}go deeper
Know what an operand-less return does: it sends back the current values of the named results. If you see one, look upward for where those variables were last assigned before you trust the exit.
Explain the mechanism behind the risk — results start at zero and stay there until assigned, so an exit that skips an assignment returns a plausible value nobody chose — and that the compiler cannot detect it.
Bring the review judgment: how far the return is from the signature, whether every path assigns every result, and why the failure surfaces as a useless message downstream rather than a crash. Say what you would ask the author to change.
Own the convention and its cost. Decide where the line sits for your codebase, what the linting story is when no standard analyzer covers it, and whether the real fix in a given package is shorter functions rather than a rule about returns.
## What a bare return actually promises When every result in a signature is named, Go permits `return` with no operands. It returns the current values of the named result variables. That is the whole rule, and it is the source of both the convenience and the hazard: **the statement's meaning depends entirely on state established elsewhere**, and the statement itself contains no evidence of what that state is. Contrast the two exits: ```go return problems, ok // this line tells you what the caller gets return // this line tells you nothing ``` Both compile, both are idiomatic Go, and in a six-line function they are equally readable because the entire body is visible at once. The difference only appears with distance. ## The three ways distance turns it into a defect **1. The names are far away.** A reader looking at the bare `return` on line 180 has to jump back to the signature to learn there even *are* two results, let alone what they are called and what they mean. Reading code is mostly local; a construct whose meaning is defined 180 lines up is a construct that forces non-local reading. **2. A skipped assignment returns a zero value nobody chose.** Every named result starts at its type's zero value and stays there until something assigns it. An early exit added months later — a guard clause, a fast path, a check for an unreadable file — bares-returns before the assignments run, and the caller receives `nil`, `0`, `false` or `""`. Consider a config validator invoked from a CI step: ```go func validate(path string) (problems []string, ok bool) { data, err := os.ReadFile(path) if err != nil { return // (nil, false): "invalid", with nothing to show } problems = checkAll(data) ok = len(problems) == 0 return } ``` The early exit is *shaped* like the happy path's exit, so a reviewer's eye slides over it. The build turns red, the step prints "config invalid", the list of problems is empty, and the engineer on the pull request cannot tell an unreadable file from a file full of mistakes. The information was available at the failing line and the return statement discarded it — the same silent-failure pattern as dropping a result with `_`, arrived at from a different direction. **3. Nothing mechanical catches it.** `gofmt` reformats it happily. The compiler is satisfied — the function returns two values of the right types on every path. Vet's standard analyzers have nothing to say about a legal return of a zero value, because a zero value is frequently the correct answer. The only line of defence is a human reading the diff, which is precisely why teams turn it into a review convention rather than hoping. ## What the convention should be The widely-held Go position is not "never name results" or "never bare-return" — it is that the two decisions are independent and should be made separately: - **Name results for documentation.** `strings.Cut(s, sep string) (before, after string, found bool)` and `io.Reader`'s `Read(p []byte) (n int, err error)` name their results so the signature explains itself. That reason applies regardless of how you return. - **Bare-return only in a function you can take in at a glance** — a handful of lines, one or two exits, all assignments visible above the return. Below that bar, write the values out. - **Never mix the forms in one function.** A body with `return a, b` in three places and a bare `return` in a fourth invites the reader to assume the fourth is the same as the others. - **Prefer shortening the function.** A 200-line function whose exits are hard to follow has a bigger problem than its return statements; extracting the branches usually makes both issues disappear at once. ## Reviewing it in practice When a bare return shows up in a pull request, the useful questions are: how far is this line from the signature, is every named result assigned on every path that reaches here, and would an explicit return read worse? The last one matters — occasionally the explicit form is genuinely noisier, in a tiny helper with three results that are all set on the line above. That case is real and rare, and it is the case the convention should permit rather than the default it should assume. The deeper point is about where the information lives. Go's design consistently prefers statements that state what they do: a `return` with operands is one of them, and a bare `return` trades that away for a few saved characters. In small functions the trade is free. In large ones you are paying for it with every subsequent read.
- If bare returns are the problem, should the team stop naming results altogether?No — the two decisions are separate. Naming results is how a signature like `(before, after string, found bool)` documents itself, and that value stands whether or not you ever write an operand-less return. The convention worth adopting is "name results freely, bare-return only in short functions".
- Would go vet or gofmt flag a bare return that ships an unassigned named result?No. The function returns values of the correct types on every path, so the compiler is satisfied, and a zero value is very often the intended answer, so there is nothing for a general analyzer to assert. It is a review-time judgment, which is why teams write it down as a convention.
- Is there a case where a bare return genuinely reads better than an explicit one?Yes, in a small helper where the results were assigned on the immediately preceding lines and the explicit form would just restate them. The test is whether the reader can see every assignment without scrolling. That case is real, and it is why the guidance is about function length rather than a blanket ban.
A bare return is like an email that says "sending the usual". Fine to your desk neighbour, useless to someone reading the thread six months later.
saying these in an interview costs you the question
- Says a bare return always returns the zero values
- Thinks the compiler rejects an unassigned named result
- Believes gofmt or vet flags the pattern
- Treats naming results and bare returns as one decision
- Defends bare returns in a 200-line function as idiomatic