Three Return Paths, One Close Button
The function below reads a config file and has three ways out — open fails, read fails, success — and each path is supposed to close the file:
func loadConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
data, err := io.ReadAll(f)
if err != nil {
f.Close() // path 2 of 3
return nil, err
}
if err := f.Close(); err != nil { // path 3 of 3
return nil, err
}
return data, nil
}
It is correct today and wrong tomorrow: the next feature adds a fourth return path, and f.Close() does not make the trip. defer solves this by attaching cleanup to the function instead of to each path:
func loadConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil
}
defer f.Close() registers the call; it executes when loadConfig returns — after the return values are set, on every path, including one added in a future edit. The rule stated precisely: defer takes a function call, that call runs when the surrounding function returns, and the surrounding function may not return until every deferred call has run.
Two consequences follow immediately. Deferred cleanup runs during a panic too: when a panic unwinds the stack, every deferred call on the way out fires in reverse order before the program crashes or a recover catches it (panic, recover, and When to Fail). And because deferred calls run before the caller resumes, they are still close enough to the return to affect what the caller ultimately receives — chapter 4 uses that deliberately.
The idiom shows up wherever a resource has a pairing operation: Lock/Unlock, Open/Close, Begin/Rollback, ticker Stop. Unlock with defer so a future early return cannot deadlock the next goroutine:
func (c *Cache) Get(key string) (Value, bool) {
c.mu.Lock()
defer c.mu.Unlock()
v, ok := c.data[key]
return v, ok
}
What defer does not do is create a scope. It does not release the resource earlier because you stopped using it, and it does not shorten the function's lifetime — everything stays alive until the function exits. If a cleanup must happen at the end of each iteration rather than the end of the function, the surrounding unit of work is too big, and the fix is in chapter 3.
Evaluated Now, Executed Later
The most-tested fact about defer is that its arguments belong to the defer statement, not to the deferred moment. Consider:
func timing() {
i := 0
defer fmt.Println("deferred sees", i) // i read here: 0
i = 5
fmt.Println("loop value is", i)
}
// output:
// loop value is 5
// deferred sees 0
The arguments to fmt.Println are evaluated when the defer statement executes — the value 0 is captured then — while the print itself waits for function exit. Nothing about i is remembered. Change the statement to defer func() { fmt.Println("deferred sees", i) }() and the body now reads i at exit, printing 5. The direct form captures values; the closure form captures the variable — the same distinction that governs closures throughout this phase (Anonymous Functions and Closures).
Multiple defers form a stack: last registered, first executed.
func order() {
fmt.Println("body")
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
}
// body
// C
// B
// A
LIFO is not a stylistic choice — it is what makes nested resource acquisition correct. Lock the mutex, then open the transaction; unlock in the opposite order, which the stack gives you for free. The classic textbook example behaves the same way:
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
// prints 2, 1, 0 when the function returns
Note what happened: three deferred calls accumulated, arguments captured as 0, 1, 2 at their statements, and nothing printed until exit — then the stack emptied in reverse. A useful mental drill: for any snippet with defers, write down (1) the order defer statements execute, (2) the moment each argument was evaluated, (3) the moment each body runs. Get those three straight and defer output questions stop being guesses.
One boundary case: the deferred call runs after the return expression is evaluated but before the caller receives control. return x computes x, then runs deferred calls, then hands the value over. That gap is small but load-bearing — it is where a deferred function can still change the answer.
The Loop That Hoards Its Cleanups
Here is the bug that ships quietly in batch jobs:
func checksumAll(paths []string) (string, error) {
var h = sha256.New()
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return "", err
}
defer f.Close() // registered 10,000 times...
if _, err := io.Copy(h, f); err != nil {
return "", err
}
}
return hex.EncodeToString(h.Sum(nil)), nil
}
defer belongs to the function, not the iteration. Every f.Close() waits for checksumAll to return, so a 10,000-file run holds 10,000 open file descriptors — until the process hits too many open files and the error path returns, at which point 10,000 closes finally fire. The cleanup is correct in bulk and wrong in time: each file should close when its iteration ends.
The fix is to make the iteration a function, so the defer has the right scope:
func checksumAll(paths []string) (string, error) {
h := sha256.New()
for _, p := range paths {
if err := hashFile(h, p); err != nil {
return "", err
}
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func hashFile(h hash.Hash, path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
}
Now each hashFile returns before the next file opens, and one defer per function does exactly what it promises. The rule generalises: defer is scoped to a function, so the function should be scoped to one unit of work. If your loop body grows past a screen, extracting it is good structure anyway — and it makes the defer correct as a side effect.
There is also a cost dimension. The compiler "open-codes" defers in small functions — a handful of defers, none in a loop — into cheap inline bookkeeping. A defer inside a loop disables that optimization: each registration allocates a record that lives until the function exits. In a hot loop that runs millions of times per second, hoarding records is a real allocation problem on top of the resource problem. Register defers at the top of a function that does bounded work; extract a helper when the work is per-iteration.
The Last Word on a Return Value
A deferred function runs after the return expression is evaluated but before the caller sees the result. When the function uses named results, that gap lets deferred code amend the answer — the mechanic behind two patterns you will read constantly.
Catching a cleanup failure. defer f.Close() throws away the error that means data was lost:
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close %s: %w", path, cerr)
}
}()
_, err = f.Write(data)
return err // deferred Close may replace this nil with a real error
}
Walk the order: return err evaluates err, the deferred closure runs and may overwrite the named result, then the caller receives the final value. The condition err == nil matters — a close failure after a write failure should not mask the original cause.
What not to do with that power. A deferred function that rewrites the result must have a reason the caller can rely on. Silently turning nil into an error, or vice versa, makes the signature lie about when failures occur. Keep the amendment local to cleanup and explain it in the error's message.
Putting the whole model together:
| Moment | What runs |
|---|---|
function body reaches return expr |
expr is evaluated |
| immediately after | deferred calls, newest first |
| during a panic | the same deferred calls, then the panic continues (or is recovered) |
| after all defers finish | caller resumes with the final values |
That table answers most questions about defer: arguments freeze at the defer statement, bodies run at exit in reverse order, and named results can still change until the last defer finishes. The pattern is the same whether the cleanup is closing a file, unlocking a mutex, or rolling back a transaction — the discipline is keeping the function's scope equal to the resource's scope. When the two disagree, change the function's shape, not the cleanup's location.
Quiz
1. What does this print? ```go func f() { i := 0 defer fmt.Println("D", i) i = 7 fmt.Println("B", i) } ```
2. A batch job processes 50,000 files with `defer f.Close()` inside the loop body and dies with 'too many open files'. What is the correct fix?
3. Why is `defer mu.Unlock()` the standard way to release a mutex in Go?
4. In `func writeReport(...) (err error)` with a deferred closure that sets `err` on Close failure, when does the caller observe the amended error?
Flashcards
Question
When are the arguments of `defer f(x)` evaluated?
Click to reveal answer
Answer
At the defer statement, not when the call later runs. f receives the value x had at registration time; to read a variable's final value instead, defer a closure: `defer func() { fmt.Println(x) }()`.
Question
In what order do deferred calls execute, and why does that order matter?
Click to reveal answer
Answer
Last-in, first-out — the most recently deferred call runs first when the function returns. That mirrors acquisition order: lock then open, so unlock and close happen in reverse, keeping nested resources balanced.
Question
What goes wrong when defer appears inside a for loop?
Click to reveal answer
Answer
The calls register against the enclosing function, so they all wait until it exits — an iteration's file stays open for the whole batch, and each registration allocates a record (disabling open-coded defer optimization). Extract the loop body into its own function so cleanup lands on the iteration.
Question
Can a deferred function change what the caller receives? How?
Click to reveal answer
Answer
Yes, with named result parameters: the return expression assigns them, deferred calls run afterward and may overwrite them, and the caller receives the final values only after all defers finish. This is how a deferred Close reports a lost-write error.
Question
Do deferred calls run when a function panics?
Click to reveal answer
Answer
Yes — a panic unwinds the stack and runs every deferred call in LIFO order on the way out, which is why `defer mu.Unlock()` still releases the lock. After the defers run, the panic continues unless a deferred recover intercepts it.
Revision Notes
Key Takeaways
- 1. defer registers a call that runs when the surrounding function returns — on every path, including panics
- 2. Arguments freeze at the defer statement; a deferred closure reads variables at exit instead
- 3. Deferred calls stack LIFO, which is exactly the reverse order resources were acquired
- 4. defer in a loop hoards resources and allocations until function exit — extract the body into a function
- 5. Named results plus a deferred closure let cleanup amend the return value before the caller sees it
Interview Tips
- • "What's the difference between `defer fmt.Println(i)` and `defer func(){ fmt.Println(i) }()`?" — argument snapshot versus late variable read; give the two-line output example
- • "Why shouldn't you defer in a loop?" — scope: defer belongs to the function, so iteration cleanups pile up; mention the resource limit and the lost open-coded optimization
- • "Does defer run during a panic?" — yes, LIFO while unwinding; follow with how recover hooks into exactly that moment
- • "Show a defer that changes the return value" — write the named-result Close pattern and note the `err == nil` guard that preserves the original failure
Cheat Sheet
defer and the Deferred Call Stack
The three moments:
| Question | Answer |
|---|---|
| When are arguments evaluated? | at the defer statement (snapshot) |
| When does the body run? | when the surrounding function returns |
| In what order? | LIFO — last deferred, first executed |
| During a panic? | same defers run while unwinding, then the panic continues |
| Before or after return-value evaluation? | after expr evaluates, before the caller resumes |
Scope rule: defer is scoped to the function. Loop body needs its own cleanup? Give it its own function. Hot loop? defer at function top, not per iteration (keeps open-coded optimization).
Patterns: unlock on exit (defer mu.Unlock()), close on exit (defer f.Close()), cleanup error into named result, deferred recover (see panic topic).
Code template:
func save(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close %s: %w", path, cerr)
}
}()
_, err = f.Write(data)
return err
}
Continue Learning
Before You Start
- No prerequisites listed.