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.
func process() {
status := "pending"
defer fmt.Println("final status:", status) // status is read RIGHT NOW, not at return
status = "done"
}
// process() prints: final status: pendingThe fix is at the bottom of the next section, once defer's actual timing rule is on the table.
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.
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) // 5Named 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.
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.
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 — LIFOThe 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:
func process() {
status := "pending"
defer func() { fmt.Println("final status:", status) }() // reads status when it RUNS
status = "done"
}
// process() prints: final status: done