skip to content

How can a deferred closure change the value a Go function has already returned?

level: middleimportance: should knowfreq 52%

answer

  1. return is not a single step
  2. the result needs a name
  3. the gap between assign and hand-off
  4. the closure takes no parameters
  5. return 5, then n++, caller sees 6

basics

~20 s

Only when the function's results are named. A return statement first assigns the result variables, then runs the deferred calls, then hands control back — so a deferred closure that assigns to a named result changes what the caller receives.

solid answer

~50 s

`return x` is not one step. It assigns `x` to the function's result variables, then runs the deferred calls, and only then returns to the caller. If the results are **named**, those variables have identifiers a deferred closure can assign to, and any change it makes lands in what the caller sees. So `func f() (n int) { defer func() { n++ }(); return 5 }` returns `6`. With unnamed results the deferred function has nothing to address: `func g() int { n := 5; defer func() { n++ }(); return n }` returns `5`, because `n` there is just a local and its value was already copied into the anonymous result. The closure must also read and write the result variable in its body — passing it as an argument would snapshot a copy at the `defer` line instead.

code

go · 10 lines
go
func plusOne() (n int) {
	defer func() { n++ }()
	return 5 // assigns n = 5, defer runs, caller gets 6
}

func unchanged() int {
	n := 5
	defer func() { n++ }()
	return n // n is a local; caller gets 5
}

go deeper

for a junior

Know that Go lets you name a function's results and that those names are ordinary variables. Be able to trace what return 5 does before the function actually hands control back.

for a middle

Explain the three steps of a return — assign results, run defers, hand off — and show the contrast between a named result the closure can write and a local it cannot. Know why passing the result as an argument defeats it.

for a senior

Show the judgment on when this earns its keep: one post-processing step covering every exit path, registered at the top of the function so a reader cannot miss it, versus the readability cost of a return line that does not say what leaves the function.

for a principal

Decide the codebase convention. This mechanism makes a function's contract non-local, so weigh a consistent, documented pattern against a rule that results are adjusted explicitly, and be ready to justify the choice to teams reading unfamiliar code.

## The three-step return The key is that a `return` statement in Go is not atomic. Executing `return x` does three things, in order: 1. assign `x` to the function's **result variables**; 2. run every pending deferred call for that function, newest first; 3. transfer control back to the caller, with whatever the result variables now hold. Step 2 sits *between* the assignment and the hand-off. That gap is the whole mechanism. ## Named results give the gap a handle Declaring results with names — `func f() (n int, ok bool)` — creates ordinary variables, initialised to their zero values at function entry and living for the whole call. A deferred closure can read and assign them like any other variable in scope: ```go func plusOne() (n int) { defer func() { n++ }() return 5 // assigns n = 5, runs the defer (n becomes 6), returns 6 } ``` The caller receives `6`. Nothing about the `return 5` line suggests that, which is exactly why interviewers like the question: it tests whether you know what `return` really does. With unnamed results there is no variable to touch: ```go func unchanged() int { n := 5 defer func() { n++ }() return n // returns 5 } ``` Here `n` is a local. `return n` copies its value into the function's anonymous result slot, and the deferred closure then increments the local — too late, and against the wrong thing. The caller gets `5`. Placing these two functions side by side is the clearest way to explain the rule. ## The closure has to do the read and the write Two details decide whether the trick actually works. First, it must be a **closure with no parameters** (or at least, the result must not be passed in). `defer adjust(n)` evaluates `n` at the `defer` line — before `return 5` has even run, so it captures the zero value and cannot write back. `defer func() { n++ }()` runs its body at return and touches the live variable. Second, the deferred call must belong to **this** function. A closure deferred inside a nested function literal adjusts that inner function's results, not the outer one's. A bare `return` in a function with named results returns whatever the named variables currently hold — the naked-return form. It is the same mechanism seen from the other side, and it is why some codebases restrict naked returns: the value that leaves the function is no longer visible on the `return` line. ## What it is good for, and what it costs The honest use is **post-processing a result on every exit path**, without repeating the logic at each `return`. A function with six returns can adjust, annotate or record its outcome once, in one deferred closure, and be sure no path escapes it. That is a real benefit: the alternative is six edits that a future contributor will make five of. The cost is readability. A reader looking at `return 5` has to notice a `defer` fifty lines above to know the caller gets `6`. The convention that keeps it manageable: the deferred closure that mutates a named result goes at the very **top** of the function, immediately after the results are declared, so the reader meets it before any `return`. And name the results meaningfully — `(n int)` is fine, `(result int)` on a function whose result is never adjusted is just noise. ## Interaction with the rest of `defer` Multiple deferred closures that touch the same named result run last-in-first-out, so the last one registered sees the value the ones after it produced — order matters, and stacking two mutators on one result is usually a sign the logic wants to be a plain helper instead. And because deferred calls run on every exit path, a mutator registered at the top applies to the guard-clause return in line three just as much as to the happy path at the bottom. That universality is the point; it is also what makes an accidental mutation so hard to spot. ## How to answer it Say the three steps of `return` out loud, then show the two-function contrast: named result gets `6`, unnamed local gets `5`. If pushed, add the argument-evaluation detail — passing the result to the deferred call captures a copy at the `defer` line and defeats the whole thing.

  • Why does `defer adjust(n)` fail to change a named result n, when `defer func() { n = adjust(n) }()` works?
    Arguments to a deferred call are evaluated at the `defer` statement. `defer adjust(n)` passes a copy of `n` as it was then — usually the zero value, since the defer is registered before any return — and whatever `adjust` computes is discarded because nothing assigns it back. The closure form runs at return, reads the current `n`, and writes the new value into the result variable itself.
  • If two deferred closures both increment the same named result, what does the caller receive?
    Both run, so both increments land, but they execute newest-first. With `defer func(){ n *= 2 }()` registered first and `defer func(){ n++ }()` second, `return 5` gives `n = 5`, then `n++` makes 6, then `n *= 2` makes 12. Order is the reverse of registration, and stacking mutators on one result is usually a sign the logic belongs in a helper.
  • Where should a deferred closure that mutates a named result be placed, and why?
    At the very top of the function, right after the results are declared. Deferred calls run on every exit path, so a mutator registered anywhere applies to every `return` — including guard clauses above it in the source. Putting it first means a reader meets the adjustment before any `return` statement and is not surprised by a value that does not match the line they are looking at.

saying these in an interview costs you the question

  • Thinks a deferred closure can change an unnamed result
  • Believes return copies the value out before defers run
  • Passes the result as an argument to the deferred call
  • Says naming results is only documentation
  • Assumes the deferred mutation applies only to the last return statement