skip to content

When are the arguments to a Go deferred call evaluated — at the `defer` line or at return?

level: middleimportance: must knowfreq 70%

answer

  1. the call waits, the values do not
  2. a snapshot is taken at that line
  3. an empty parameter list captures nothing
  4. time.Since read far too early
  5. wrap it in a closure

basics

~20 s

At the defer line. Go evaluates the deferred call's function value, receiver and arguments immediately and stores those values; only the invocation waits until the function returns. Wrap the call in a closure when you need values read later.

solid answer

~40 s

The arguments — and the receiver, and the function value itself — are evaluated **when the `defer` statement executes**, not when the call finally runs. Go snapshots them there and postpones only the invocation. So `i := 0; defer fmt.Println(i); i = 42` prints `0`, and `defer fmt.Println(time.Since(start))` reports a near-zero duration because `time.Since` was called at the top of the function. When you want values read at return time, defer a closure that takes no arguments: `defer func() { fmt.Println(time.Since(start)) }()` runs its whole body later and reads the variables then. The same rule explains why `defer mu.Unlock()` is safe even if `mu` is a field on a struct you later reassign: the receiver was captured at the `defer` line.

code

go · 8 lines
go
func work() {
	start := time.Now()
	// argument evaluated here, so this reports a near-zero duration
	defer fmt.Println("A:", time.Since(start))
	// body runs at return, so this reports the real elapsed time
	defer func() { fmt.Println("B:", time.Since(start)) }()
	time.Sleep(50 * time.Millisecond)
}

go deeper

for a junior

Remember the short version: whatever you pass to a deferred call is fixed at the moment you write the defer. Be able to say that i := 0; defer fmt.Println(i); i = 42 prints 0.

for a middle

Explain the mechanism: function value, receiver and arguments are all evaluated eagerly and stored, and only the invocation is postponed. Show the closure form as the deliberate way to move the read to return time.

for a senior

Show that you catch this in review — a deferred elapsed-time log that always reports zero, a deferred method whose receiver is reassigned later, a nil receiver whose panic surfaces at return instead of at the line that registered it.

for a principal

Set the house style: which instrumentation is deferred and which is explicit, and whether wrapping every defer in a closure to be safe is worth the extra noise in code other teams read and copy.

## The rule When a `defer` statement executes, Go evaluates three things straight away: the **function value** being called, the **receiver** if it is a method call, and every **argument**. Those values are saved. What is postponed is only the act of calling. So `defer` is not "run this expression later". It is "call *this function*, with *these exact values*, later". ## The canonical demonstration ```go i := 0 defer fmt.Println(i) // prints 0 i = 42 ``` At the `defer` line, `i` is `0`, so `0` is handed to `fmt.Println` and stored. Reassigning `i` afterwards changes the variable, not the saved argument. At return, `fmt.Println(0)` runs. Contrast a deferred closure with no parameters: ```go i := 0 defer func() { fmt.Println(i) }() // prints 42 i = 42 ``` Here the argument list is empty, so there is nothing to snapshot. The closure body is what runs at return, and it reads `i` at that moment — by then, `42`. The distinction is not about closures being magic; it is that a closure moves the read *inside the deferred call*, and the deferred call happens later. ## The timing trap this rule creates The most common real-world bite is elapsed-time logging: ```go start := time.Now() defer log.Printf("took %s", time.Since(start)) // WRONG: near-zero, every time ``` `time.Since(start)` is an argument, so it is called at the `defer` line, microseconds after `start` was taken. The log line will faithfully report a duration close to zero no matter how long the function runs. The fix is to move the computation into the deferred call: ```go start := time.Now() defer func() { log.Printf("took %s", time.Since(start)) }() ``` A related idiom exploits the rule deliberately: ```go defer trace("work")() ``` Here `trace("work")` runs **now** (it can log "entering" and return a function), and the function it returns is what gets deferred. Two calls, two different times, on one line — legal precisely because the deferred call's function value is evaluated eagerly. ## Receivers follow the same rule `defer w.Flush()` on a `*bufio.Writer` variable `w` evaluates `w` at the `defer` line. If you later point `w` at a different writer, the deferred call still flushes the original one. Usually that is what you want; occasionally it is a subtle bug, and the cure is the same closure. The eager evaluation also means the pointer is dereferenced later, not now: `defer w.Flush()` on a nil `w` registers happily and fails only when the deferred call actually runs, at return, far from the line a reader blames. ## Why the language works this way Eager argument evaluation makes the deferred call **deterministic and independent of everything the rest of the function does**. If arguments were evaluated at return, the meaning of a `defer` at the top of a long function would depend on every assignment below it, and cleanup registered against a resource could silently retarget itself. Snapshotting binds the intent at the point of registration, which is the same instinct as writing `defer f.Close()` immediately under the `os.Open` — the resource you meant is the resource you get. It also composes with loops and with re-registration: each execution of a `defer` statement captures its own values, so two iterations that defer the same call with different arguments really do queue two distinct calls. ## How to choose in practice Use a plain deferred call — `defer f.Close()`, `defer mu.Unlock()`, `defer resp.Body.Close()` — when the values are already final at the `defer` line. That is most cleanup, and it is the clearer form. Use a deferred closure when the deferred code must read something that is still going to change: elapsed time, a counter, a named result, a value assigned later in the function. A useful review heuristic: if any identifier in the deferred call's arguments is assigned again below that line, the plain form is probably wrong. ## What to say in an interview State the rule in one sentence ("arguments, receiver and function value are evaluated at the `defer` statement; only the call is postponed"), show the `i = 42` pair, and name the closure as the deliberate escape hatch. Interviewers are checking that you understand `defer` schedules a *call*, not an *expression*.

  • Why does `defer trace("work")()` call something immediately and something else at return?
    Because the deferred call's function value is evaluated eagerly. `trace("work")` runs at the `defer` line and returns a function; that returned function is what is registered and invoked at return. It is the standard enter/exit instrumentation idiom, and it works only because Go evaluates the callee — not just the arguments — at the `defer` statement.
  • If `defer w.Flush()` is written while w is a nil *bufio.Writer, when does that fail?
    At return, not at the `defer` line. Registering the call only evaluates and stores the receiver, so a nil receiver is recorded without complaint; the dereference happens when the deferred call finally runs. That is why such failures surface at the end of the function, far from the line a reader would blame — check the receiver before deferring the call.
  • Does the eager-evaluation rule change anything when the same defer statement executes twice?
    Each execution snapshots its own values and queues its own call. Two executions of `defer log.Print(id)` with different `id` values register two separate calls that will print the two different values, newest first. The rule is per-execution, not per-source-line.

It is like addressing an envelope now and posting it later: the address is fixed the moment you write it, even if the recipient moves before it is sent.

saying these in an interview costs you the question

  • Says arguments are evaluated when the deferred call finally runs
  • Claims defer captures variables by reference like a closure
  • Uses defer log.Printf with time.Since and expects real elapsed time
  • Thinks the receiver of a deferred method call is resolved at return
  • Believes reassigning a variable cancels or updates a pending deferred call