Skip to Content
Go Realm v1 is released 🎉
TopicsDefer

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 defer like 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
RuleDescription
LIFO orderMultiple defers run in reverse registration order
Arguments evaluated immediatelyArgs are captured at the defer line, not at call time
Runs on any exitNormal return, early return, error, or panic

Execution Flow Trace

StepLine executedDefer stack (top = runs first)Output
1fmt.Println("start")[]start
2defer fmt.Println("deferred 1")[d1]
3defer fmt.Println("deferred 2")[d2, d1]
4fmt.Println("end")[d2, d1]end
5function 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 value

Plain Arg vs Closure — Side by Side

defer fmt.Println(x)defer func() { fmt.Println(x) }()
How x is capturedValue copy at defer lineReference to x
Sees later changes to x❌ No✅ Yes
When x is readAt defer statementWhen deferred func executes
Use whenYou want the value right nowYou 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()) // 10

Why the difference?

Named returnUnnamed return
return X semanticsresult = 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 returns nil.


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: 15

Named Return — Step-by-Step Trace

StepActionresult valueReturn slotOutput
1result initialized (zero value)0start: 0
2defer registered (closure, captures result ref)0
3result = 55before return: 5
4return → writes result into return slot55
5defer runs: result += 10result = 151515 ← shared!defer: 15
6function exits, caller receives return slot15
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 ran

Unnamed Return — Step-by-Step Trace

StepActionresult (local)Return slotOutput
1result := 00start: 0
2defer registered (closure, captures result ref)0
3result = 55before return: 5
4return resultcopies 5 into anonymous slot55 ← locked!
5defer runs: result += 10result = 15155 (unchanged)defer: 15
6function exits, caller receives return slot5

Key insight: Named return shares the variable with the return slot. Unnamed return copies first — defer modifies the local copy but the slot is already locked.


8. Common Mistakes ⚠️

MistakeProblemFix
defer inside a loopdefers pile up until function exitswrap body in anonymous func
Assuming defer captures variablesargs are captured, closures capture referencesuse explicit arg passing
recover() outside deferred funcalways returns nilonly call recover inside defer func(){}()
Defer for expensive operationsoverhead from building defer stackskip defer in tight hot-path loops
Closing resource without checking open errorcloses nil handle → panicalways 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 defer registered runs first.

Q: Are defer arguments evaluated immediately?

Yes. Arguments to the deferred function are evaluated at the defer statement. 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 returns nil.

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.