Go defer: when arguments are evaluated, what order calls run in, and how results change

Go's defer evaluates arguments when the defer statement runs and calls deferred functions in reverse order just before the function returns. Here is why that lets them change named results.

godeferlanguage-specclosures

When you write defer f(x) in Go, f is called later. But x is evaluated now, not later. That difference is why a log line can show an unexpected value, or why a caller can receive 30 from a function that executed return 3. This post explains the behavior using only the rules in the Go language specification.

The rules

The “Defer statements” section of the Go spec says:

  • The expression after defer must be a function or method call, and it cannot be parenthesized.
  • When the defer statement executes, the function value and the arguments are evaluated and saved. The function itself is not called at that point.
  • The saved call runs immediately before the surrounding function returns. That covers executing a return statement, reaching the end of the function body, and the goroutine panicking.
  • Multiple deferred calls run in the reverse order they were deferred.
  • When the function returns through an explicit return, the return sets the result parameters first, then the deferred functions run, and then control goes back to the caller.

Effective Go adds that for a method, the receiver is evaluated along with the arguments at the time of the defer. Also, defer works per function, not per block. A call deferred inside an if block runs when the surrounding function returns, not when the block ends.

Example

The program below shows three of the rules at once.

package main

import "fmt"

func show(label string, v int) {
	fmt.Println(label, v)
}

func compute() (n int) {
	x := 1
	defer show("argument:", x)
	defer func() { show("closure:", x) }()
	defer func() { n *= 10 }()
	x = 2
	return 3
}

func main() {
	fmt.Println("result:", compute())
}

The output below is derived from the spec rules. No Go toolchain was available in this environment, so it was not observed from a run.

closure: 2
argument: 1
result: 30

Step by step:

  1. When the first defer show("argument:", x) executes, x is 1. That 1 is saved as the argument.
  2. The second defer saves a function literal with no arguments. A function literal is a closure, so it shares the variable x with the surrounding function instead of copying it. When this closure runs later, it reads whatever x holds at that moment.
  3. The third defer saves a closure that multiplies the result parameter n by 10.
  4. x = 2 executes.
  5. return 3 sets n to 3.
  6. The deferred calls run in reverse order. First n becomes 30. Next the closure prints the current x, which is 2. Last, show is called with the saved argument 1.
  7. compute returns the value of n, which is 30.

The difference between argument: 1 and closure: 2 is the main point. An argument is evaluated at the time of the defer and that value is saved, so in this example the integer 1 is used. A variable read inside a closure is read when the closure runs. Saving an argument is not a deep copy, though. If you pass a value that refers to other data, such as a pointer, slice, or map, the saved value is that reference, so later changes to the referenced data can still be visible to the deferred call.

Why named result parameters can be changed

According to the “Return statements” section of the spec, named result parameters act as ordinary local variables, and they are initialized to the zero value of their type when the function is entered. A return statement that specifies results sets the result parameters before any deferred functions run.

So if a deferred function literal is in a position where those result parameters are in scope, it can read or change the return value that has already been set. The spec includes an example where return 6 is followed by a deferred closure that runs result *= 7, and the function returns 42. By the same rule, if you name the err result parameter, a deferred closure can inspect err and replace it with a different value.

If the results are unnamed, the deferred function has no variable name to refer to, so it cannot change the return value this way. And if a deferred function itself returns values, those values are discarded. A deferred function’s own return value cannot change the surrounding function’s result.

Things that are easy to miss

  • A nil function value: if the deferred function value is nil, the panic happens when the function would be invoked, not at the defer statement.
  • defer inside a loop: each iteration adds one more call, and they all run in reverse order when the function returns, not when the loop ends. The spec example for i := 0; i <= 3; i++ { defer fmt.Print(i) } prints 3, 2, 1, 0 in that order.
  • When arguments are evaluated: if you want the later value, read the variable inside a closure instead of passing it as an argument. If you want the value as evaluated now, pass it as an argument. Passing a pointer, slice, or map does not make a copy of the data it refers to.

Scope and sources

This post is based on the Go language specification as checked on 2026-10-13 (the page reads: Language version go1.27, May 26, 2026). It does not cover which version introduced each rule. It also does not cover recover, the performance of defer, or compiler optimizations and other implementation details. The example output is derived from the spec rules and is not the result of a run.

The links below were checked on 2026-10-13.

Back to the blog