In Go, what does `return 5` compile to in a function with deferred calls and a named result?
answer
- return is not one instruction
- three steps, defers sit in the middle
- the result slot is written first
- a name is what makes the slot reachable
- arguments are captured too early to help
basics
~20 sIt compiles to three steps: assign 5 into the named result's slot, run the frame's deferred calls, then return to the caller. Because the defers run in between, a deferred closure writing to that named result changes what the caller receives.
solid answer
~50 sA `return` statement in Go is not a single machine step. The compiler turns `return 5` into: (1) copy 5 into the function's result slot, (2) run the deferred calls registered for this frame, in reverse order, (3) actually return to the caller, which then reads the result slot. When the result is *named*, that slot has an identifier the deferred closure can assign to, so `func f() (n int) { defer func() { n++ }(); return 5 }` returns 6 — the deferred call ran between the assignment and the jump. With an unnamed result there is no name in scope, so no deferred call can reach the slot: `func g() int { n := 5; defer func() { n++ }(); return n }` returns 5, because `n` is an ordinary local that was copied into the slot before the defer ran. That asymmetry is the whole reason error-wrapping and cleanup idioms insist on a named result.
code
go · 12 linesfunc f() (n int) {
defer func() { n++ }()
return 5 // n = 5, then the defer makes it 6
}
func g() int {
n := 5
defer func() { n++ }()
return n // the anonymous result slot already holds 5
}
// f() returns 6; g() returns 5go deeper
Remember that deferred calls run after the return value has been set but before the caller gets control, and that only a named result can be changed by them.
Be able to write both variants on the whiteboard and explain the three-step sequence: assign the result slot, run the defers in reverse order, then return.
Demonstrate judgment about when the trick is worth using. Modifying a named result in a deferred closure is powerful for uniform post-processing and dangerous when it is hidden far from the return statements.
Decide where this pattern is allowed in the codebase. It makes cross-cutting result handling uniform, but it also makes a function's outcome unreadable from its return statements, so codify which packages may use it and require the named result to be obvious.
## `return` is three things Go's specification describes a return in a function with deferred calls as a sequence, and the compiler implements exactly that sequence: 1. **Assign the returned expressions to the result parameters.** Every Go function has storage for its results, whether or not you gave those results names. `return 5` writes 5 there. 2. **Run the deferred calls for this frame**, last registered first. 3. **Return to the caller**, which reads the result storage. Step 2 sitting between steps 1 and 3 is the entire mechanism behind "a deferred function can change the return value". Nothing magic is happening: the deferred call runs while the frame and its result storage are still alive, and if it can name that storage, it can write to it. ## Named versus unnamed results The only difference between a named and an unnamed result is whether the result storage has an identifier that code inside the function body can refer to. ```go func f() (n int) { defer func() { n++ }() return 5 } // f() == 6 ``` Here `n` *is* the result slot. `return 5` sets it to 5, the deferred closure increments it to 6, and the caller reads 6. ```go func g() int { n := 5 defer func() { n++ }() return n } // g() == 5 ``` Here `n` is an ordinary local variable and the result slot is anonymous. `return n` copies 5 out of `n` into that anonymous slot, and only then does the deferred closure run and bump the local to 6. The caller still reads the copy, which is 5. The closure did exactly what it said; it just wrote to the wrong storage. Spelling the two out side by side is the fastest way to show an interviewer that you understand the *sequence* rather than a rule you memorised. ## A bare `return` with named results A naked `return` in a function with named results skips step 1's copy — the slots already hold whatever the body last assigned to them — and goes straight to running defers and returning. This is why a naked return plus a deferred mutation reads confusingly: nothing in the `return` statement mentions the value the caller gets. Most style guidance restricts naked returns to very short functions for exactly that reason. ## The closure has to be a closure A subtlety that catches people: a deferred call's *arguments* are evaluated at the `defer` statement, so `defer report(n)` captures the value of `n` as it was then and cannot influence the result. Only a closure that refers to the named result by name — `defer func() { n++ }()` — reads and writes the live slot at the moment the defers run. "Pass it as an argument" and "capture it in a closure" look similar in source and behave completely differently here. ## Multiple deferred calls All of the frame's deferred calls run during step 2, in reverse registration order, and each one sees whatever the previous ones left in the result slots. So two deferred closures that both modify a named result compose, with the *last registered* getting first look and the *first registered* having the final word. If you find yourself relying on that ordering to compute a return value, the function is usually asking to be restructured. ## Why this exists at all The design gives Go a place to run code that both observes and adjusts a function's outcome, without adding a language construct for it. Post-processing a result, adjusting a value on the way out, or attaching context to what the function is about to return all become ordinary deferred closures over a named result. The cost is that a reader has to know the three-step sequence to predict what a function returns — which is precisely why it is a standard interview question. ## The one-line summary to say out loud "`return x` assigns to the result slot, *then* runs the defers, *then* returns. If the result has a name, a deferred closure can still change what the caller sees; if it does not, the value was already copied out of reach."
- Why can't `defer func(n int) { n++ }(n)` change the returned value?Because a deferred call's arguments are evaluated at the `defer` statement, so that closure receives a copy of `n` taken before the return even happened, and increments its own parameter. Only a closure that refers to the named result by name touches the live result slot when the defers run. The distinction between capturing by reference and passing by value decides the whole outcome here.
- What does a naked `return` do in a function with named results and defers?It skips the assignment step, because the named result slots already hold whatever the body last stored in them, then runs the deferred calls and returns. The deferred calls can still modify those slots, so the caller can receive a value that appears nowhere near the `return` statement — which is why naked returns are usually confined to very short functions.
- If two deferred closures both modify the same named result, which one wins?They all run, last registered first, each seeing what the previous ones left behind, so the *first* registered closure runs last and has the final word on the slot's value. Relying on that ordering to compute a return value is legal but hard to read, and is usually a sign the logic belongs in the function body rather than spread across deferred calls.
Think of the result slot as an envelope on the desk: return fills it, the deferred calls get one last look before it leaves the room, and only a named result gives them the address to write on it.
saying these in an interview costs you the question
- Says the deferred call runs after the caller resumes
- Claims a deferred closure can change an unnamed result
- Thinks return copies the value after the defers run
- Believes defer cannot see the result at all
- Confuses capturing a named result with passing it as an argument