← thecodex.expert · The Codex Family of Knowledge
Go

Functions and defer

Functions can return more than one value with no wrapper object needed, and defer guarantees cleanup code runs no matter how the function exits.

Go 1.26 defer is LIFO Last verified:
Where this bites you

You write a function that logs a job's status on the way out, so you can see what happened no matter which return statement fired: status := "pending"; defer fmt.Println("final status:", status), then update status as the function runs. The log always prints pending — even on the run where the job genuinely finished as done. The log isn't lying about when it ran. It's lying about what it saw.

Godeferbug.go
func process() {
    status := "pending"
    defer fmt.Println("final status:", status)   // status is read RIGHT NOW, not at return
    status = "done"
}
// process() prints: final status: pending

The fix is at the bottom of the next section, once defer's actual timing rule is on the table.

Canonical Definition

Go functions natively support multiple return values — no tuple type or wrapper object required — which is how the idiomatic value, err := doThing() pattern works. The defer statement schedules a function call to run immediately before the enclosing function returns, regardless of which return statement fired or whether a panic occurred; multiple deferred calls run in last-in-first-out (LIFO) order.

Multiple return values

Any Go function can return more than one value, declared in the signature as a comma-separated list. This is the mechanism behind Go's error-handling convention: a function that can fail returns its normal result alongside an error, and the caller checks the error explicitly rather than catching an exception.

Gofunctions.go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %v by zero", a)
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result)   // 5

Named return values

A function's return values can be given names in the signature, which pre-declares them as local variables initialized to their zero value. A bare return with no arguments then returns whatever those named variables currently hold — useful for short functions but easy to make confusing in long ones, so idiomatic Go uses named returns sparingly.

Gonamed_returns.go
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return   // returns x and y automatically
}

defer: guaranteed cleanup, LIFO order

Here is the rule the bug at the top of this page runs into: a deferred call's arguments are evaluated the instant the defer statement executes, not when the call actually runs later. Only the call itself is postponed. defer f.Close() is fine because f doesn't change; defer fmt.Println("final status:", status) is not, because status is read immediately, while it still says "pending".

A defer call is scheduled to run right before its enclosing function returns — whether that's via a normal return, an early return, or a panic unwinding the stack. This makes it Go's idiomatic way to guarantee cleanup (closing a file, unlocking a mutex, closing a database connection) happens exactly once, right next to where the resource was acquired. Multiple defers in the same function run in last-in-first-out order: the most recently deferred call runs first.

Godefer.go
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()   // guaranteed to run when readFile returns, however it returns

    // ... use f ...
    return nil
}

func order() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// order() prints: 3, 2, 1 — LIFO

The fix for the status-log bug is to defer a closure instead of a direct call — the closure's body isn't evaluated until it runs, so it reads status at return time, not at the defer line:

Godefer_fix.go
func process() {
    status := "pending"
    defer func() { fmt.Println("final status:", status) }()   // reads status when it RUNS
    status = "done"
}
// process() prints: final status: done

Sources

1
The Go Authors. The Go Programming Language Specification, "Defer statements", go.dev/ref/spec#Defer_statements.