defer in Go
defer schedules a function call to run when the surrounding function returns — regardless of whether it returns normally, with an error, or via panic.
🧹 Analogy: Think of
deferlike a sticky note on the exit door. No matter how you leave the room, the note always gets executed on your way out.
1. The Three Rules of defer
func main() {
fmt.Println("start")
defer fmt.Println("deferred 1")
defer fmt.Println("deferred 2")
fmt.Println("end")
}
// Output:
// start
// end
// deferred 2 ← LIFO
// deferred 1| Rule | Description |
|---|---|
| LIFO order | Multiple defers run in reverse registration order |
| Arguments evaluated immediately | Args are captured at the defer line, not at call time |
| Runs on any exit | Normal return, early return, error, or panic |
Execution Flow Trace
| Step | Line executed | Defer stack (top = runs first) | Output |
|---|---|---|---|
| 1 | fmt.Println("start") | [] | start |
| 2 | defer fmt.Println("deferred 1") | [d1] | — |
| 3 | defer fmt.Println("deferred 2") | [d2, d1] | — |
| 4 | fmt.Println("end") | [d2, d1] | end |
| 5 | function returns → drain stack | [d1] | deferred 2 |
| 6 | [] | deferred 1 |
2. Arguments Evaluated Immediately ⭐
func main() {
x := 10
defer fmt.Println("x =", x) // x captured as 10 RIGHT NOW
x = 20
fmt.Println("x is now", x)
}
// Output:
// x is now 20
// x = 10 ← not 20!But a closure captures the variable reference, not the value:
func main() {
x := 10
defer func() { fmt.Println("x =", x) }() // captures reference
x = 20
}
// Output:
// x = 20 ← sees the updated valuePlain Arg vs Closure — Side by Side
defer fmt.Println(x) | defer func() { fmt.Println(x) }() | |
|---|---|---|
How x is captured | Value copy at defer line | Reference to x |
Sees later changes to x | ❌ No | ✅ Yes |
When x is read | At defer statement | When deferred func executes |
| Use when | You want the value right now | You want the value at return time |
3. Common Use: Cleanup
// File
func readFile(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // guaranteed to run no matter what
// ... read file
return nil
}
// Mutex
func (s *Store) update(key string, val int) {
s.mu.Lock()
defer s.mu.Unlock() // always unlocked, even on panic
s.data[key] = val
}
// Timing
func track(name string) func() {
start := time.Now()
return func() { fmt.Printf("%s took %v\n", name, time.Since(start)) }
}
// Usage:
defer track("processOrder")()4. defer + Named Returns — The Powerful Trick ⭐
defer can read and modify named return variables because they share the same memory slot.
// Named return — defer CAN modify the return value
func namedReturn() (result int) {
defer func() { result = 99 }()
return 10 // sets result=10, then defer overwrites to 99
}
fmt.Println(namedReturn()) // 99
// Unnamed return — defer CANNOT modify the return value
func unnamedReturn() int {
result := 10
defer func() { result = 99 }() // modifies local var, not return slot
return result // copies 10 into return slot; defer runs after but it's too late
}
fmt.Println(unnamedReturn()) // 10Why the difference?
| Named return | Unnamed return | |
|---|---|---|
return X semantics | result = X (shared var) | copies X to anonymous return slot |
defer can modify | ✅ Yes — same variable | ❌ No — return slot already copied |
Real production use — wrapping errors:
func doWork() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("doWork: %w", err) // wrap with context
}
}()
return riskyOperation()
}5. defer + panic / recover
defer is the only way to recover from a panic.
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered panic: %v", r)
}
}()
return a / b, nil // panics if b == 0
}
res, err := safeDiv(10, 0)
fmt.Println(res, err) // 0, recovered panic: runtime error: integer divide by zero
recover()only works inside a deferred function — calling it elsewhere returnsnil.
6. defer in a Loop — The Trap ⚠️
// ❌ Defers pile up — all run when the FUNCTION returns, not per iteration
for i := 0; i < 1000; i++ {
f, _ := os.Open(files[i])
defer f.Close() // 1000 files stay open until function exits!
}
// ✅ Wrap in a function — defer runs per call
for i := 0; i < 1000; i++ {
func(path string) {
f, _ := os.Open(path)
defer f.Close() // closes at end of THIS anonymous func
}(files[i])
}7. Guess the Output — Named vs Unnamed ⚠️
func named() (result int) {
fmt.Println("start:", result) // 0
defer func() {
result += 10
fmt.Println("defer:", result)
}()
result = 5
fmt.Println("before return:", result) // 5
return
}
// Output:
// start: 0
// before return: 5
// defer: 15
// caller gets: 15Named Return — Step-by-Step Trace
| Step | Action | result value | Return slot | Output |
|---|---|---|---|---|
| 1 | result initialized (zero value) | 0 | — | start: 0 |
| 2 | defer registered (closure, captures result ref) | 0 | — | — |
| 3 | result = 5 | 5 | — | before return: 5 |
| 4 | return → writes result into return slot | 5 | 5 | — |
| 5 | defer runs: result += 10 → result = 15 | 15 | 15 ← shared! | defer: 15 |
| 6 | function exits, caller receives return slot | — | 15 | — |
func unnamed() int {
result := 0
fmt.Println("start:", result) // 0
defer func() {
result += 10
fmt.Println("defer:", result)
}()
result = 5
fmt.Println("before return:", result) // 5
return result
}
// Output:
// start: 0
// before return: 5
// defer: 15
// caller gets: 5 ← return slot copied BEFORE defer ranUnnamed Return — Step-by-Step Trace
| Step | Action | result (local) | Return slot | Output |
|---|---|---|---|---|
| 1 | result := 0 | 0 | — | start: 0 |
| 2 | defer registered (closure, captures result ref) | 0 | — | — |
| 3 | result = 5 | 5 | — | before return: 5 |
| 4 | return result → copies 5 into anonymous slot | 5 | 5 ← locked! | — |
| 5 | defer runs: result += 10 → result = 15 | 15 | 5 (unchanged) | defer: 15 |
| 6 | function exits, caller receives return slot | — | 5 | — |
Key insight: Named return shares the variable with the return slot. Unnamed return copies first —
defermodifies the local copy but the slot is already locked.
8. Common Mistakes ⚠️
| Mistake | Problem | Fix |
|---|---|---|
defer inside a loop | defers pile up until function exits | wrap body in anonymous func |
Assuming defer captures variables | args are captured, closures capture references | use explicit arg passing |
recover() outside deferred func | always returns nil | only call recover inside defer func(){}() |
| Defer for expensive operations | overhead from building defer stack | skip defer in tight hot-path loops |
| Closing resource without checking open error | closes nil handle → panic | always check error before defer |
9. Interview Cheat Sheet
Q: When does a deferred function run?
When the surrounding function returns — after the return value is set but before the caller receives it. It runs on any exit path: normal return, early return, or panic.
Q: What order do multiple defers run in?
LIFO (Last In, First Out) — the last
deferregistered runs first.
Q: Are defer arguments evaluated immediately?
Yes. Arguments to the deferred function are evaluated at the
deferstatement. A closure captures a reference and sees later changes; a plain argument captures the current value.
Q: Can defer modify a function’s return value?
Only if the function uses named return values. The deferred function shares the named variable and can overwrite it before the caller sees the result. With unnamed returns, the value is already copied to the return slot before defer runs.
Q: How do you recover from a panic?
Call
recover()inside a deferred function. If the function panicked,recover()returns the panic value and stops the unwinding. Outside a defer,recover()always returnsnil.
Q: Why is defer in a loop dangerous?
Defers accumulate on the stack and all fire when the function returns — not per iteration. In a loop that opens files, all handles stay open until the function exits. Fix: wrap the loop body in an anonymous function.