A Go defer statement evaluates its function value and call parameters when execution reaches the statement, even though the deferred function runs only as the surrounding function returns. Mutations between those two moments do not retroactively change already saved argument values.
This split between evaluation and invocation is part of the language semantics rather than an optimization detail. Each executed defer records a call with values established at that point. Return processing later invokes recorded calls in reverse registration order.
Argument values belong to the registration point
A variable passed directly as an argument is evaluated before subsequent statements can change it:
func emit() {
n := 4
defer fmt.Println(n)
n = 9
}The deferred call prints 4. The assignment changes n, but the call parameter was already evaluated and saved.
The same boundary applies to expressions with visible effects:
func closeLater() {
defer release(acquire())
work()
}acquire() runs when the defer statement executes. Only release(...) waits for function return. Code that treats the entire call expression as delayed can therefore move allocation, locking, logging, or other effects to an earlier point than intended.
Closures move variable reads to invocation time
A function literal can produce different behavior because the deferred call itself may have no parameter carrying the changing value:
func emit() {
n := 4
defer func() {
fmt.Println(n)
}()
n = 9
}This version prints 9. Registration evaluates the function value, while the body reads n later when the deferred function runs.
The distinction is between a saved argument value and a variable referenced by a closure. It is not a special exception in defer; the two forms evaluate different expressions at registration time.
A parameterized closure restores value capture at registration:
func emit() {
n := 4
defer func(v int) {
fmt.Println(v)
}(n)
n = 9
}Here n is an argument to the deferred call, so its value is saved as 4.
Method receivers are part of the saved call
Receiver evaluation follows the same rule as ordinary call parameters. A deferred method call binds the receiver when the defer executes.
For a value receiver, that can preserve a value copy that differs from later state:
type Counter struct {
n int
}
func (c Counter) report() {
fmt.Println(c.n)
}
func run() {
c := Counter{n: 4}
defer c.report()
c.n = 9
}The deferred call reports 4 because the value receiver was evaluated as part of the call registration.
A pointer receiver saves the pointer value rather than a snapshot of the pointed-to object. Mutating the same object before return remains visible through that pointer:
func (c *Counter) report() {
fmt.Println(c.n)
}
func run() {
c := &Counter{n: 4}
defer c.report()
c.n = 9
}This form reports 9. The saved receiver still points at the same Counter.
Reassignment and mutation are separate effects
Saving a pointer, slice, map, channel, function, or interface value does not deep-copy the data reachable through that value. The argument itself is fixed, while referenced state may continue to change.
func run() {
values := []int{4}
defer fmt.Println(values)
values[0] = 9
}The saved slice value contains its data pointer, length, and capacity. Updating the backing array changes what the deferred formatter later observes.
Reassigning the local slice variable is different:
func run() {
values := []int{4}
defer fmt.Println(values)
values = []int{9}
}The deferred argument retains the earlier slice value, so the later variable assignment does not replace the saved argument.
This value-versus-referent boundary is especially relevant for cleanup calls. Saving a handle value protects the deferred call from later reassignment of the local variable, but it does not freeze mutable state behind that handle.
Return values are set before deferred calls run
Deferred invocation occurs after result parameters are assigned by a return statement and before control reaches the caller. That ordering allows a closure to observe or modify named result parameters:
func result() (n int) {
defer func() {
n *= 2
}()
return 6
}The return statement assigns 6 to n; the deferred closure then changes it to 12.
Direct arguments still retain registration-time semantics:
func result() (n int) {
defer fmt.Println(n)
n = 6
return
}The deferred print receives the value of n from the point where defer executed, not the value present during return processing.
Reverse invocation does not reverse evaluation
Multiple defers run in last-in, first-out order, but their arguments are evaluated in normal execution order as each statement is reached:
defer record(first())
defer record(second())first() executes before second() during registration. At return, record for the second saved value runs before record for the first saved value.
Evaluation order and deferred invocation order therefore form two distinct timelines. Side effects in argument expressions occur on the forward path through the function; deferred function bodies run on the reverse path during return.
That separation is the central boundary of defer: registration performs call evaluation and saves the resulting function value and parameters, while return processing performs the invocation.