Skip to content

defer evaluates its arguments now

Level: 201 · anyone whose deferred log line printed a stale value, or whose deferred method reported the wrong state

One line: A defer statement saves the function value and every argument — the receiver included — the moment it runs, and only the call waits for the function to return; a closure, by contrast, reads its variables when it finally runs.

The program defers the same print in the two forms, then a method on a value receiver and on a pointer receiver, and last a function held in a variable that is reassigned after the defer. Each function prints its own "at exit" line just before returning, so the deferred line follows it.

Verified output of defer_evaluates_its_arguments_now_go.go — regenerated by tools/run_examples.py, never hand-typed.

argumentIsSavedNow: handled = 2 at exit
  defer fmt.Println(handled) printed handled = 1
closureReadsAtExit: handled = 2 at exit
  the closure printed handled = 2
receiverIsSavedNow: t.orders = 5 at exit
  ReportLive (pointer receiver) saw orders = 5
  Report (value receiver) saw orders = 1
functionValueIsSavedNow: report now names the second report
  ran the first report

Reading the output

defer fmt.Println(handled) printed 1. The spec ↗ says that each time a defer statement executes, the function value and the parameters to the call are evaluated as usual and saved anew, but the function is not invoked. handled was 1 at the defer; the 2 assigned afterwards was never seen.

The closure printed 2. func() { fmt.Println(handled) } takes no arguments, so there was nothing to save; the closure reads handled when it runs, at exit. This is the form for the value at exit — a deferred log of the result, a recover, a named result.

Report saw 1, ReportLive saw 5. A method call's receiver is one of its arguments. defer t.Report() on a value receiver copied t at the defer; the later t.orders = 5 changed the original, not the copy. defer t.ReportLive() on a pointer receiver saved &t, and the field was read through it at exit. Effective Go ↗ says it in one line: the arguments to the deferred function, which include the receiver if the function is a method, are evaluated when the defer executes.

The first report ran. report was a variable holding a function; defer report() saved the function value it held at that moment. Reassigning the variable afterwards changes what a later call would do, not what the deferred call does.

defer_evaluates_its_arguments_now_go.go

defer_evaluates_its_arguments_now_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// A defer statement saves the function value and its arguments the moment it
// runs; only the call itself waits for the surrounding function to return. A
// closure reads its variables when it runs, so the two forms print different
// values. A method's receiver is an argument too.
//
//  go build defer_evaluates_its_arguments_now_go.go && ./defer_evaluates_its_arguments_now_go
package main

import "fmt"

// Tally counts the orders a worker has handled.
type Tally struct{ orders int }

// Report has a value receiver: defer t.Report() copies t at the defer statement.
func (t Tally) Report() { fmt.Println("  Report (value receiver) saw orders =", t.orders) }

// ReportLive has a pointer receiver: the pointer is saved now, the field is read at exit.
func (t *Tally) ReportLive() { fmt.Println("  ReportLive (pointer receiver) saw orders =", t.orders) }

func argumentIsSavedNow() {
    handled := 1
    defer fmt.Println("  defer fmt.Println(handled) printed handled =", handled)
    handled = 2
    fmt.Println("argumentIsSavedNow: handled =", handled, "at exit")
}

func closureReadsAtExit() {
    handled := 1
    defer func() { fmt.Println("  the closure printed handled =", handled) }()
    handled = 2
    fmt.Println("closureReadsAtExit: handled =", handled, "at exit")
}

func receiverIsSavedNow() {
    t := Tally{orders: 1}
    defer t.Report()
    defer t.ReportLive()
    t.orders = 5
    fmt.Println("receiverIsSavedNow: t.orders =", t.orders, "at exit")
}

func functionValueIsSavedNow() {
    report := func() { fmt.Println("  ran the first report") }
    defer report()
    report = func() { fmt.Println("  ran the second report") }
    fmt.Println("functionValueIsSavedNow: report now names the second report")
}

func main() {
    argumentIsSavedNow()
    closureReadsAtExit()
    receiverIsSavedNow()
    functionValueIsSavedNow()
}

What to do

  • Defer a plain call when the arguments are ready now: defer f.Close(), defer mu.Unlock(), defer wg.Done(). The receiver is fixed at the defer, which is what you want for a file or a lock.
  • Defer a closure when you need the value at exit: a named result, a recover, an elapsed time — the closure reads them when it runs.
  • Watch value receivers. defer t.Report() on a value receiver reports the t of the defer line. If the method is meant to see later changes, give it a pointer receiver or defer a closure.
  • Read defer trace("x")() as two calls. The outer one runs now and its result is the function deferred — the kata below builds on exactly that.

Practice

Write trace(name string) func() that prints enter name when called and returns a function that prints leave name. Use it as defer trace("...")() in three nested functions and show, with indentation, that the enter lines print at the defer statements and the leave lines at the returns, innermost first.

Solution

defer_evaluates_its_arguments_now_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Kata: a trace helper. `defer trace("name")()` prints "enter" at once,
// because the outer call is an argument and defer evaluates arguments now;
// the function it returns prints "leave" when the caller returns. Nested
// calls show their nesting.
package main

import "fmt"

var depth int

func trace(name string) func() {
    fmt.Printf("%*senter %s\n", depth*2, "", name)
    depth++
    return func() {
        depth--
        fmt.Printf("%*sleave %s\n", depth*2, "", name)
    }
}

func processOrder(id int) {
    defer trace(fmt.Sprintf("processOrder(%d)", id))()
    checkStock(id)
    charge(id)
}

func checkStock(id int) {
    defer trace("checkStock")()
    fmt.Printf("%*sorder %d: 3 items in stock\n", depth*2, "", id)
}

func charge(id int) {
    defer trace("charge")()
    fmt.Printf("%*sorder %d: charged\n", depth*2, "", id)
}

func main() {
    processOrder(18)
}

Verified output of defer_evaluates_its_arguments_now_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.

enter processOrder(18)
  enter checkStock
    order 18: 3 items in stock
  leave checkStock
  enter charge
    order 18: charged
  leave charge
leave processOrder(18)

In other languages

  • Rust has no defer. Cleanup is Drop, which runs when the owner's scope ends, and a value moved into a guard is captured at that moment — the Rust library's Scope is about names, not values ↗ shows what a scope end drops and in which order.
  • Python runs the body of a finally: block at exit (the try statement ↗), and a with block calls __exit__ on the object it entered — both read their variables when they run, like Go's closure form; nothing is saved early.
  • C++ runs destructors at scope end (RAII ↗); a destructor sees the object's state at that moment, like the pointer receiver here.

Sources

  • The Go specification, Defer statements ↗ — the sentence on when the function value and parameters are evaluated.
  • Effective Go, "Defer" ↗ — the trace/un example the kata is modelled on, and the note on receivers.
  • Defer, Panic, and Recover ↗ — Andrew Gerrand, the Go blog, 4 August 2010: the first of its three rules is that a deferred function's arguments are evaluated when the defer statement is evaluated.
  • Alan A. A. Donovan and Brian W. Kernighan, The Go Programming Language (Addison-Wesley, 2015), chapter 5, "Functions" — section 5.8, "Deferred Function Calls".
  • Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 5, "Functions" — the section "defer".