Go Defer — Junior Level¶
1. Introduction¶
What is it?¶
A defer statement schedules a function call to run just before the surrounding function returns — no matter how it returns (normal return, explicit return, or panic). It is Go's primary mechanism for guaranteed cleanup: closing files, unlocking mutexes, decrementing wait groups, releasing handles.
You can think of defer as "do this on the way out". Once you write defer f.Close() after opening a file, you no longer have to remember to close it on every return path. The runtime does it for you.
How to use it?¶
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("data.txt")
if err != nil {
fmt.Println(err)
return
}
defer f.Close() // runs when main returns, even on a later panic
// ... read from f ...
}
When main returns, the runtime walks the deferred-call stack of the current goroutine and executes them in last-in-first-out order.
2. Prerequisites¶
- Functions basics (2.6.1)
- Multiple return values (2.6.3)
- Anonymous functions (2.6.4)
- Pointers and method receivers (for
defer m.Unlock()style)
3. Glossary¶
| Term | Definition |
|---|---|
| defer | A statement that schedules a call to execute when the surrounding function returns |
| deferred call | A call queued by defer, waiting to run on function exit |
| LIFO | "Last in, first out" — the order deferred calls execute |
| argument evaluation | The point at which defer's arguments are computed (at defer-time, not call-time) |
| panic | An abnormal termination signal; deferred calls still run |
| recover | A built-in that stops a panic; only effective inside a deferred call |
| named return value | A return parameter declared in the signature; deferred calls can modify it |
| open-coded defer | A Go 1.14+ optimization that inlines defers when their count is bounded and small |
4. Core Concepts¶
4.1 LIFO Execution Order¶
Deferred calls run in the reverse order they were registered. The most recently deferred call runs first.
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("main body")
}
Output:
You can picture a stack of pending calls. Each defer pushes onto the stack. On function exit, the runtime pops them off and runs each one.
4.2 Arguments Are Evaluated At defer-Time¶
This is the single biggest source of confusion for newcomers. The arguments to a deferred function are computed immediately — at the moment the defer runs — not at the moment the deferred function eventually executes.
package main
import "fmt"
func main() {
x := 10
defer fmt.Println("deferred x =", x) // x evaluated NOW; "10" is captured
x = 99
fmt.Println("end of main, x =", x)
}
Output:
Even though we mutated x to 99 before the function returned, the deferred fmt.Println already had 10 baked into its argument list.
If you want the deferred call to see the latest value, wrap it in a closure:
The closure captures x by reference (per Go closure semantics), so it reads x at call-time.
4.3 Deferred Calls Run On Panic Too¶
When a goroutine panics, the runtime walks up the call stack and executes deferred calls along the way. This is what makes defer safe for cleanup: even if your code later explodes, the file still closes, the lock still unlocks, the cleanup still happens.
package main
import "fmt"
func main() {
defer fmt.Println("clean up 1")
defer fmt.Println("clean up 2")
panic("boom")
}
Output (before the runtime prints the panic trace):
4.4 Defer + Named Return Values¶
A deferred call can modify the function's return value when the return values are named. This is a common pattern for wrapping errors or computing results in cleanup code.
package main
import "fmt"
func add(a, b int) (sum int) {
defer func() {
sum *= 2 // modifies the named return value AFTER `return` has been evaluated
}()
sum = a + b
return // returns 2 * (a + b)
}
func main() {
fmt.Println(add(3, 4)) // 14
}
The mental model: return assigns to the named return values, then deferred calls run, then control leaves the function.
If the return values are unnamed, you cannot reach them from a deferred call.
4.5 The Resource Cleanup Pattern¶
The canonical use of defer is right next to the acquisition of a resource:
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
Reading top-to-bottom, you immediately see "opened, will close". You don't need to remember to call Close() on every return path; one defer covers all of them.
4.6 Defer Inside Loops Is A Trap¶
Defers do not run at the end of each iteration — they run at the end of the enclosing function. A loop that defers per iteration accumulates deferred calls, which can leak handles, exhaust file descriptors, or balloon memory.
// BAD: opens many files; none closes until the function returns.
func processAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // accumulates one per iter
// ... use f ...
}
return nil
}
The fix is to extract the per-iteration work into its own function:
func processOne(p string) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // runs at end of processOne, every iter
// ... use f ...
return nil
}
func processAll(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
Each processOne call has its own defer scope.
5. Real-World Analogies¶
Hotel checkout list: when you check in, you write down "return key card" on a list. As you do other things in the hotel (check in, eat, sleep), you don't worry about the key. On checkout, the front desk reads the list bottom-up and reminds you of every promise. You always return the key, even if you're rushing out.
Return paperwork after a meeting: imagine a meeting with three pieces of paperwork to file as you leave (badge, receipt, security tag). You note them on your way in. You don't have to remember on the way out — the system files them in reverse order.
Restaurant check at the end of a meal: regardless of whether you finish the meal, walk out, or get sick mid-meal, the bill closes. The kitchen doesn't have to track every possible exit; closing happens on the way out.
6. Mental Models¶
function entry
│
▼
┌──────────────────────┐
│ defer A() │ ── push A
│ defer B() │ ── push B
│ defer C() │ ── push C
│ ... body ... │
└──────────────────────┘
│
▼
(return or panic)
│
▼
pop & run C(), then B(), then A()
│
▼
leave function
A more concrete picture: each goroutine has a small linked list of pending defers attached to its g struct. Each defer X(args) allocates (or reuses) a record holding the function pointer and a snapshot of arguments. Function exit walks the list.
7. Pros & Cons¶
Pros¶
- Cleanup is local. The acquire and release sit on adjacent lines.
- Cannot be skipped accidentally by an early return.
- Runs on panic, so resources are released during failure paths too.
- Reads top-to-bottom without scanning every return statement.
- Works with named return values to wrap errors or transform results.
Cons¶
- Per-call cost (~30 ns historically; ~3-7 ns with Go 1.14 open-coded defer in common cases).
- Does NOT scope to a loop iteration — each defer is tied to the enclosing function.
- Argument evaluation timing is a common gotcha.
- Stack traces include the deferred frames, which can clutter panics.
- Hot paths sometimes need
unlock(); ...; lock()instead ofdefer unlock().
8. Use Cases¶
- File / connection close:
defer f.Close(),defer conn.Close(),defer rows.Close(). - Mutex unlock:
mu.Lock(); defer mu.Unlock(). - Wait-group decrement:
wg.Add(1); go func() { defer wg.Done(); ... }(). - Tracing:
defer trace("operation")()— measure how long a function ran. - Panic recovery:
defer func() { if r := recover(); r != nil { ... } }(). - Error wrapping: a deferred closure can mutate a named
errreturn value. - Counters and metrics: increment on entry, decrement on exit via defer.
- Rolling back transactions:
defer tx.Rollback()(commit clears it implicitly via state).
9. Code Examples¶
Example 1 — File Cleanup¶
package main
import (
"io"
"os"
)
func readAll(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
Example 2 — Mutex Unlock¶
package main
import "sync"
type SafeCounter struct {
mu sync.Mutex
n int
}
func (c *SafeCounter) Incr() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
Example 3 — LIFO Demo¶
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
defer fmt.Println("deferred", i)
}
fmt.Println("main body")
}
// main body
// deferred 3
// deferred 2
// deferred 1
Example 4 — Argument Evaluation Time¶
package main
import "fmt"
func main() {
x := 1
defer fmt.Println("at defer-time x =", x) // captures 1
x = 100
defer func() { fmt.Println("at exit x =", x) }() // reads at exit
x = 999
}
// at exit x = 999
// at defer-time x = 1
Example 5 — Panic-Safe Cleanup¶
package main
import "fmt"
func dangerous() {
defer fmt.Println("cleanup ran")
panic("oops")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
dangerous()
}
// cleanup ran
// recovered: oops
Example 6 — Modifying Named Return Value¶
package main
import "fmt"
func divide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("divide panicked: %v", r)
}
}()
return a / b, nil
}
func main() {
fmt.Println(divide(10, 0)) // 0 divide panicked: runtime error: integer divide by zero
}
Example 7 — Trace Helper¶
package main
import (
"fmt"
"time"
)
func trace(name string) func() {
start := time.Now()
fmt.Println("enter", name)
return func() {
fmt.Println("exit", name, "took", time.Since(start))
}
}
func work() {
defer trace("work")()
time.Sleep(50 * time.Millisecond)
}
func main() { work() }
The trick: trace("work") runs immediately and returns a closure. defer ...() defers calling that returned closure on exit.
10. Coding Patterns¶
Pattern 1 — Acquire/Release¶
Pattern 2 — Open/Close¶
Pattern 3 — Two-Phase Commit¶
tx, err := db.Begin()
if err != nil { return err }
defer func() {
if err != nil {
tx.Rollback()
}
}()
// ... work that may set err ...
return tx.Commit()
Pattern 4 — Error Annotation¶
func work() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("work: %w", err)
}
}()
// ... work that may set err ...
return nil
}
Pattern 5 — Trace¶
11. Clean Code Guidelines¶
- Defer immediately after acquisition.
f, _ := os.Open(p); defer f.Close()belongs on adjacent lines. - Always check the error before deferring
Close— otherwise you mightf.Close()on anilf. - Keep deferred calls cheap — they run on every exit path.
- Avoid defers in tight loops — extract a helper function instead.
- Use a closure if you need to read late-bound state.
- Don't mix recover with non-deferred functions —
recoveronly works inside a deferred call. - Prefer
defer mu.Unlock()over manual unlocks in non-hot paths.
12. Common Mistakes (Buggy + Fixed)¶
Mistake 1 — Closing an unchecked file¶
Buggy:
Fixed:
Always confirm the resource exists before deferring its release.
Mistake 2 — Defer inside a loop¶
Buggy:
for _, p := range paths {
f, err := os.Open(p)
if err != nil { return err }
defer f.Close() // accumulates; doesn't run until function ends
// ... use f ...
}
Fixed: extract to a helper.
func handle(p string) error {
f, err := os.Open(p)
if err != nil { return err }
defer f.Close()
// ... use f ...
return nil
}
Mistake 3 — Expecting a captured arg to update¶
Buggy:
i := 0
defer fmt.Println("i =", i) // captures 0
for ; i < 5; i++ {}
// expected "i = 5"; actually prints "i = 0"
Fixed: wrap in a closure to read late:
Mistake 4 — recover outside a deferred function¶
Buggy:
Fixed: put recover inside a deferred function.
Mistake 5 — Forgetting that defer doesn't see updates to non-pointer args¶
Buggy:
func write(b *bytes.Buffer) {
defer log("buf size", b.Len()) // b.Len() runs NOW; later writes invisible
b.WriteString("hello")
}
Fixed:
func write(b *bytes.Buffer) {
defer func() { log("buf size", b.Len()) }() // evaluated at exit
b.WriteString("hello")
}
13. Mini Exercises¶
Exercise 1 — LIFO Order¶
What does this print?
Exercise 2 — Argument Evaluation¶
What does this print?
Answer
`x` is evaluated when `defer` runs, capturing `1`. The later assignment to `2` does not change the captured value.Exercise 3 — Closure vs Argument¶
Predict the output.
func main() {
x := 1
defer func() { fmt.Println("closure:", x) }()
defer fmt.Println("arg:", x)
x = 999
}
Answer
The "arg" form captures `x` at defer-time (value 1). The closure form reads `x` at call-time (value 999). LIFO orders the closure first to register, last to execute — so it runs after `arg`, but I wrote them in the order *closure first, then arg*. Re-check: `defer func()` was registered first; `defer fmt.Println("arg:", x)` was registered second. LIFO: arg runs first, then closure. Final order: `arg: 1` then `closure: 999`.Exercise 4 — File Close¶
Write a function that opens a file, reads its bytes, and ensures the file is closed regardless of the read result.
Solution
Exercise 5 — Named Return Modification¶
Without changing main, make compute return 200.
Solution
The deferred closure modifies the named return value `n` after `return 100` has assigned it.14. Cheat Sheet¶
// Cleanup
f, _ := os.Open(p)
defer f.Close()
// Locking
mu.Lock()
defer mu.Unlock()
// LIFO
defer A()
defer B() // runs first
// Args evaluated NOW
defer fmt.Println(x) // snapshot of x
// Args evaluated LATER (closure)
defer func() { fmt.Println(x) }() // sees latest x
// Modify named return value
func f() (err error) {
defer func() { err = fmt.Errorf("f: %w", err) }()
...
}
// Recover (must be in deferred func)
defer func() {
if r := recover(); r != nil {
// handle
}
}()
// Avoid in loops
for _, p := range paths {
handle(p) // handle uses defer internally
}
15. Self-Assessment Checklist¶
- I can describe the LIFO execution order
- I know
defer's arguments are evaluated at thedeferstatement, not at call-time - I can explain when to use a closure vs a direct call
- I know deferred calls run on panic
- I can use
recovercorrectly inside a deferred function - I know how to modify a named return value from a deferred call
- I avoid
deferinside loops by extracting helper functions - I always verify the resource exists before deferring its release
16. Summary¶
defer schedules a function call to run when the surrounding function returns — whether by normal return, explicit return, or panic. Deferred calls run in LIFO order. Their arguments are evaluated at defer-time, not call-time; wrap in a closure to read late-bound state. Deferred calls can modify named return values, which is the basis of Go's idiomatic error-wrapping pattern. Use defer for resource cleanup, mutex unlocking, panic recovery, and tracing. Avoid placing defer inside loops; instead, extract per-iteration logic into a helper.
17. Further Reading¶
- Effective Go — Defer
- Go Spec — Defer statements
- Go Blog — Defer, panic, recover
- Go 1.14 release notes — Open-coded defer
18. Related Topics¶
- 2.6.4 Anonymous Functions
- 2.6.5 Closures (capture semantics applied to deferred closures)
- 2.6.6 Named Return Values
- Chapter 7 Concurrency (mutex + defer pattern)
- Panic / Recover (covered alongside defer in many texts)