---
title: Deferred Function Argument Evaluation
description: How Go evaluates arguments to deferred functions and the pitfalls and patterns that arise from immediate evaluation
---
When Go encounters a `defer` statement, it does not simply remember a function to call later. It first evaluates every argument to that function right away, then stores the call — arguments and all — until the surrounding function returns. The call itself runs later, but the values the call will use are fixed at the moment `defer` is reached.
This split between immediate argument capture and delayed execution is the core of how `defer` works. It makes cleanup predictable, but it also catches beginners off guard when a variable changes after the `defer` line but before the function exits.
## How Deferred Argument Evaluation Works
Two distinct steps happen every time a `defer` statement runs:
1. **Argument evaluation:** The Go runtime evaluates every expression passed to the deferred function. These values are copied and stored alongside the function pointer on a stack private to the current function call.
2. **Queuing:** The call is pushed onto that stack. It will not execute until the function is about to return, either normally or through a panic.
This means the arguments are *frozen* at the point of declaration, not at the point of execution.
```go
package main

import "fmt"

func main() {
    x := 10
    defer fmt.Println("Value of x:", x)
    x = 25
}

Immediate capture:

The argument x in defer fmt.Println("Value of x:", x) is evaluated the moment that line runs. Changing x afterward has no effect on the deferred call — it already holds the value 10.

The output is:

Value of x: 10

If fmt.Println had waited and read x at the end, the output would be 25. Understanding this difference is the starting point for avoiding nearly every common defer mistake.

Variables, Pointers, and What “Immediate” Really Means

Beginners often ask: if arguments are captured immediately, how can a deferred function ever see updated state? The answer depends on whether you hand the function a value or a reference to that value.

Passing by Value

When you pass a variable directly, Go copies its current value. Later mutations to the variable are invisible.

func demonstrateValue() {
    counter := 0
    defer fmt.Println("Counter at exit:", counter)
    counter++
    counter++
}

This prints Counter at exit: 0. The copy was made before the increments.

Passing a Pointer

When you pass a pointer, the pointer itself is captured immediately, but the data it points to is read later, at execution time.

func demonstratePointer() {
    balance := 100
    p := &balance
    defer fmt.Println("Balance at exit:", *p)
    balance -= 40
}

Output: Balance at exit: 60 The pointer p is fixed, but dereferencing happens when the deferred call runs. That lets the call observe changes made after the defer line.

Pointer semantics can hide bugs:

If you need a snapshot of a value at the point of deferral, do not pass a pointer to a variable that will change. Instead, copy the value into a new variable and pass that.

Slices and Maps

Slices and maps behave like pointers in the way they share underlying data. When you pass a slice, the slice header (which contains a pointer to the backing array) is captured immediately. Changes to the elements of the slice will be visible to the deferred call.

func demonstrateSlice() {
    data := []int{1, 2, 3}
    defer fmt.Println("Slice contents:", data)
    data[1] = 99
}

Output: Slice contents: [1 99 3] The same principle applies to maps: modifications to the map entries will be seen by the deferred call because the map header references the same underlying hash table. A mental model that helps: think of defer as taking a photograph of the arguments. If the argument is a value, the photo holds that value. If the argument is a pointer, slice, or map, the photo holds the directions to the data, not the data itself.

The Loop Variable Trap

One of the most frequently reported surprises involves using defer inside a loop with a closure. The behavior differs sharply between deferring a direct function call with an immediate argument and deferring an anonymous function that captures the loop variable by reference.

Direct Argument in Loop

When you pass the loop variable directly as an argument, each iteration evaluates it right away and stores a copy.

for i := 1; i <= 3; i++ {
    defer fmt.Println("Direct:", i)
}

Output (reversed because of LIFO order):

Direct: 3
Direct: 2
Direct: 1

Each line shows the value of i at the moment that particular defer ran.

Closure Capturing Loop Variable

When you defer an anonymous function that references the loop variable without passing it as an argument, the function reads the variable at execution time, not at iteration time.

for i := 1; i <= 3; i++ {
    defer func() { fmt.Println("Closure:", i) }()
}

In Go versions before 1.22, this prints:

Closure: 4
Closure: 4
Closure: 4

The loop variable i has a single address for the whole loop. By the time the deferred functions run, the loop has finished and i is 4 (the value that caused the loop to terminate). Go 1.22 changed loop variable semantics so that each iteration gets its own i. With Go 1.22+, the output becomes 3, 2, 1, matching the direct argument case.

Loop variable semantics vary by Go version:

If you are writing code that must compile with older Go versions (before 1.22), always pass the loop variable as an explicit argument to the deferred function, or capture it in a local variable inside the loop body.

The safe pattern that works everywhere:

for i := 1; i <= 3; i++ {
    i := i // create a new variable scoped to this iteration
    defer func() { fmt.Println("Safe:", i) }()
}

The Tracing Function Pattern

A practical application of deferred argument evaluation is building function tracing — logging when a function starts and ends, often with the elapsed time. The pattern uses defer on the return value of a helper function.

package main

import (
    "fmt"
    "time"
)

func trace(name string) func() {
    start := time.Now()
    fmt.Printf("Entering %s\n", name)
    return func() {
        fmt.Printf("Exiting %s (%s)\n", name, time.Since(start))
    }
}

func processOrder(id int) {
    defer trace("processOrder")()
    // simulate work
    time.Sleep(150 * time.Millisecond)
    fmt.Printf("Order %d processed\n", id)
}

func main() {
    processOrder(42)
}

Here is the execution sequence step by step:

  1. defer trace("processOrder")() is reached.
  2. Go immediately calls trace("processOrder"). Inside trace:
    • start captures the current time.
    • "Entering processOrder" is printed.
    • A closure that prints the exit message is returned.
  3. The returned closure is the actual deferred function. Its call is queued.
  4. time.Sleep and the order processing print happen.
  5. Just before processOrder returns, the deferred closure runs, printing the exit message with the elapsed time. Output:
Entering processOrder
Order 42 processed
Exiting processOrder (150.234ms)

Why this pattern works:

The tracing function captures the start time and the function name immediately. The returned closure — the deferred part — runs later with those captured values still intact. This is a clean way to instrument function calls without repeating boilerplate at every exit point.

This pattern relies on understanding that arguments to trace (here "processOrder") are evaluated right away, and that trace itself returns a function that will be deferred. If you accidentally write defer trace("processOrder") (without the trailing ()), you would be deferring the call to trace itself, not its return value, and the pattern would break.

Method Receivers and Function Values

The immediate evaluation rule also applies to the receiver of a deferred method call and to the function value in a deferred call expression.

Method Receiver Evaluation

When you defer a method call like defer obj.Method(args...), the receiver obj and all arguments are evaluated immediately.

package main

import "fmt"

type Counter struct {
    value int
}

func (c Counter) Show(tag string) {
    fmt.Printf("%s: %d\n", tag, c.value)
}

func main() {
    c := Counter{value: 10}
    defer c.Show("Direct method")   // c is captured at value 10
    c.value = 999
    c.Show("Before return")
}

This prints:

Before return: 999
Direct method: 10

The receiver c is a value, so a copy of the struct is made at the defer line. The later mutation to c.value does not affect the deferred call. If you want the deferred method to see the updated state, pass a pointer receiver:

func (c *Counter) Show(tag string) {
    fmt.Printf("%s: %d\n", tag, c.value)
}

Now the pointer is captured immediately, but the field c.value is read when the deferred call runs, so the output of the deferred line would be 999.

Deferred Function Values

The function value itself — the thing being called — is also evaluated at the defer statement.

package main

import "fmt"

func main() {
    f := func() { fmt.Println("first") }
    defer f()
    f = func() { fmt.Println("second") }
}

This prints first. The assignment to f after the defer does not change which function was queued.

Nil function values panic at execution time:

If the function value is nil when it is deferred, the panic occurs when the deferred call actually runs, not when defer is encountered.

package main

import "fmt"

func main() {
    defer fmt.Println("This runs before the panic")
    var f func() // nil
    defer f()    // the panic will happen here, at function exit
    fmt.Println("Main ending")
}

Output:

Main ending
This runs before the panic
panic: runtime error: invalid memory address or nil pointer dereference

The defer f() line itself does not panic. It stores the nil function on the defer stack. The panic happens when that call is invoked during the return phase.

Built-in Functions and Defer

Some built-in functions cannot be deferred directly because their result values cannot be discarded, and the result of a deferred call must be discarded. A notable example is append when its return value is needed.

s := []string{"a", "b", "c", "d"}
// This is invalid if we need the result:
// defer append(s[:1], "x", "y")

The workaround is to wrap the call in an anonymous function:

defer func() {
    _ = append(s[:1], "x", "y")
}()

For copy and recover, the same principle applies: wrap them in a function literal.

Built-in function restrictions:

In practice, the need to defer calls like append or copy is rare. When it does arise, the anonymous function wrapper is the idiomatic solution.

Resource Lifecycle and the Defer Stack

Deferred calls run in last‑in‑first‑out order, which matches the natural lifecycle of nested resources: open a connection, open a transaction, prepare a statement — then close them in reverse. The argument evaluation rule guarantees that each defer captures the resource handle at the moment it was opened, even if the variable is reused later in the function.

func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // captures f immediately

    // later: f could theoretically be reassigned, but the deferred call
    // still closes the original file.
    ...
}

Large defer stacks in loops:

If you open resources inside a loop and defer their cleanup, all those deferred calls pile up until the function returns. For a loop that processes thousands of files, this can exhaust file descriptors or memory. Wrap each iteration in an anonymous function to trigger cleanup at the end of each iteration.

// Problematic:
for _, file := range files {
    f, err := os.Open(file)
    if err != nil {
        return err
    }
    defer f.Close() // deferred until function return
    // process file
}

// Better:
for _, file := range files {
    if err := func() error {
        f, err := os.Open(file)
        if err != nil {
            return err
        }
        defer f.Close()
        // process file
        return nil
    }(); err != nil {
        return err
    }
}

Summary

Deferred function argument evaluation in Go follows one simple rule: arguments are evaluated when the defer statement runs, not when the deferred function runs. This rule applies to every argument, every method receiver, and even the function value being deferred. The immediate consequence is that value arguments are snapshots, while pointer, slice, and map arguments carry a reference that can reflect later changes. Closures deferred inside loops can behave unexpectedly unless the loop variable is captured explicitly. The tracing function pattern exploits immediate evaluation to log entry and exit cleanly, and the LIFO execution order naturally handles layered resource cleanup. When you need to late bind — seeing the latest state of a variable at the moment the function exits — wrap the logic in an anonymous function literal and defer that. This gives you control over when each piece of information is evaluated.