Introduction
defer schedules a function call to run just before the surrounding function returns. If you think it's just "a function that runs at the end," you're missing the important details about how Go actually executes your code.
This guide explains what defer really does and how it works at the runtime level.
Quick Mental Model
Before diving deep, understand this fundamental truth:
-
return x(unnamed): The value is copied immediately, frozen in a register -
returnwith named result: The variable stays mutable until the function exits
This single insight explains 90% of defer behavior.
What defer Does
defer stores a function call and executes it when the function returns.
func basicExample() {
defer fmt.Println("This runs last")
fmt.Println("This runs first")
// Output:
// This runs first
// This runs last
}
What actually happens:
- The function call is stored (not executed)
- Execution continues normally
- At return time, all deferred calls execute
Argument Evaluation vs Execution Time
Arguments are evaluated immediately, not when the deferred function runs.
func argumentTiming() {
i := 1
defer fmt.Println("Deferred print:", i) // i is evaluated NOW (value 1)
i = 2
// Output: Deferred print: 1
}
The value 1 is captured when defer is encountered. Changing i later has no effect.
Stack Frames and Execution
When a function is called:
- A stack frame is created
- Local variables are allocated
- A defer list pointer is attached to the frame
When defer is encountered, the function call is stored in the defer list:
┌─────────────────────┐
│ Stack Frame │
├─────────────────────┤
│ Local Variables │
│ Return Address │
│ Defer List Pointer──┼──→┌─────────────┐
└─────────────────────┘ │ Defer Node │
├─────────────┤
│ Function │
│ Arguments │
│ Next ───────┼─→ [More Defers]
└─────────────┘
LIFO Order: Last-In, First-Out
Multiple defer statements execute in reverse order:
func lifoExample() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
// Output:
// Third
// Second
// First
}
The last defer added is the first one to execute.
Named vs Unnamed Return Values
This is the core concept. Named and unnamed returns behave differently with defer.
Unnamed Return (Value is Captured)
func unnamedReturn() int {
x := 5
defer func() {
x = x + 10 // Too late to affect return value
}()
return x // Value 5 is captured here
}
// Returns: 5
When return x executes, Go immediately captures the value 5. Deferred functions run after, but they can't change what's already captured.
Named Return (Variable is Shared)
func namedReturn() (result int) {
defer func() {
result = result + 10 // Modifies the return variable!
}()
result = 5
return // Returns the modified value
}
// Returns: 15
result is a variable in the stack frame that deferred functions can access and modify until the function actually returns.
Deep Dive: Named vs Unnamed Returns with Closures
Example 1: Named Return
package main
import "fmt"
func calculate() (result int) {
result = 0
fmt.Println("first", result)
show := func() {
result = result + 10
fmt.Println("defer", result)
}
defer show()
result = 5
p := func(a int) {
fmt.Println("me", a)
}
defer p(result)
defer fmt.Println(result)
fmt.Println("second", result)
defer fmt.Println(5)
return result
}
func main() {
fmt.Println("calculate result:", calculate())
}
Output:
first 0
second 5
5
me 5
5
defer 15
calculate result: 15
Explanation:
-
result = 0→ prints "first 0" -
defer show()→ closure registered (captures reference toresult) result = 5-
defer p(result)→ function called with value 5 (argument captured immediately) -
defer fmt.Println(result)→ value 5 captured -
defer fmt.Println(5)→ literal 5 -
fmt.Println("second", result)→ prints "second 5" -
return result→ return statement reached
Defers execute in LIFO order:
-
defer fmt.Println(5)→ "5" -
defer fmt.Println(result)→ "5" -
defer p(result)→ "me 5" -
defer show()→ closure modifiesresultto 15, prints "defer 15"
Return value becomes 15 because the named return variable was modified by the closure before the function actually returned.
Example 2: Named vs Unnamed Comparison
package main
import "fmt"
// Named return: defer can modify it
func calculate() (result int) {
result = 0
fmt.Println("first", result)
show := func() {
result = result + 10
fmt.Println("defer", result)
}
defer show()
result = 5
fmt.Println("second", result)
return result
}
// Unnamed return: defer cannot modify it
func calc() int {
result := 0
fmt.Println("first", result)
show := func() {
result = result + 10
fmt.Println("defer", result)
}
defer show()
result = 5
fmt.Println("second", result)
return result
}
func main() {
fmt.Println("calculate result:", calculate())
fmt.Println("calc result:", calc())
}
Output:
first 0
second 5
defer 15
calculate result: 15
first 0
second 5
defer 15
calc result: 5
The Difference:
| Aspect |
calculate() (Named) |
calc() (Unnamed) |
|---|---|---|
| Return Value | 15 | 5 |
| What Happens | Closure modifies return variable | Closure modifies local variable |
| Return Mechanism | Returns modified variable | Returns captured value |
Execution Timeline
Named Return (calculate()):
1. Enter function → result = 0 (named return in stack frame)
2. Register defer show() → closure captures reference to 'result'
3. Set result = 5
4. Return statement reached → process defer list
5. Execute show() → result = 15
6. Return final value of 'result' = 15
Unnamed Return (calc()):
1. Enter function → result = 0 (local variable)
2. Register defer show() → closure captures reference to 'result'
3. Set result = 5
4. Return statement reached → value 5 captured in register
5. Process defer list → execute show() → result = 15 (local changed)
6. Return value from register = 5 (unchanged!)
Understanding Closures in Defer
Deferred closures capture variables by reference, not by value:
func closureExample() (result int) {
defer func() {
result = result * 2 // Shares the same memory address
}()
result = 5
return // Returns 10, not 5
}
The closure has access to the actual result variable and can modify it.
Reference vs Copy Semantics
| Scenario | Behavior | Example |
|---|---|---|
Direct value in defer
|
Copy | defer fmt.Println(x) |
| Closure capturing variable | Reference | defer func() { use(x) }() |
| Named return variable | Reference | func f() (x int) |
| Unnamed return value | Copy (captured) | return x |
func referenceVsCopy() {
x := 1
defer fmt.Println("Copy:", x) // Captures value 1
x = 2
y := 1
defer func() {
fmt.Println("Reference:", y) // Will see y=2
}()
y = 2
}
// Output:
// Reference: 2
// Copy: 1
How Go Implements defer Internally
Go implements defer using a singly linked list where each function's stack frame maintains a pointer to a chain of defer records.
Defer Record Allocation
When defer is encountered, Go:
- Creates a new defer record (stack- or heap-allocated depending on escape analysis)
- Stores the function pointer and captured arguments
- Links it to the defer chain starting from the most recent addition
This means each defer statement prepends to the chain, naturally achieving LIFO order.
Defer Record Structure
A conceptual view of the defer record structure:
// Simplified conceptual structure
// (actual runtime definitions vary by Go version)
type _defer struct {
fn func() // Function to call
args []uintptr // Captured argument values
next *_defer // Pointer to next defer in chain
}
Note: This is a simplified conceptual structure; actual runtime definitions vary by Go version and may include additional fields for panic handling and frame pointers.
Execution Flow
Stack Frame
├─ Local Variables
├─ Return Address
└─ Defer List Pointer ──→ [Defer Record #3] (most recent)
├─ Function: show()
├─ Arguments: captured values
└─ Next ──→ [Defer Record #2]
├─ Function: p()
├─ Arguments: 5
└─ Next ──→ [Defer Record #1]
├─ Function: fmt.Println()
├─ Arguments: 5
└─ Next ──→ NULL
When the function returns, execution starts at the head (most recently added defer) and follows the chain:
func example() {
defer fmt.Println("1") // Record 1
defer fmt.Println("2") // Record 2 (becomes head)
defer fmt.Println("3") // Record 3 (new head)
// At return:
// Head → Record 3 (execute) → Record 2 (execute) → Record 1 (execute) → NULL
// Output:
// 3
// 2
// 1
}
Why This Design?
- Per-function management: Each function tracks its own defer chain independently
- Dynamic size: No fixed limit on defer count; scales with runtime behavior
- O(1) insertion: Adding a new defer is a single pointer operation
- Natural LIFO: Prepending to the head automatically gives reverse execution order
- Closure support: Each record can capture different closure environments
- Panic handling: The defer chain is traversed even during panic recovery
Common Pitfalls
Loop Variable Capture
// WRONG: All closures see final value of i
func problematicLoop() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i) // Prints: 3, 3, 3
}()
}
}
// RIGHT: Pass as parameter to capture value
func fixedLoop() {
for i := 0; i < 3; i++ {
defer func(n int) {
fmt.Println(n) // Prints: 2, 1, 0
}(i)
}
}
Resource Cleanup in Loops
// WRONG: All Close() calls happen at end of function
func processFiles(filenames []string) {
for _, name := range filenames {
f, _ := os.Open(name)
defer f.Close() // Accumulates
}
}
// RIGHT: Use a wrapper function for scope
func processFilesCorrect(filenames []string) {
for _, name := range filenames {
func() {
f, _ := os.Open(name)
defer f.Close() // Closes immediately after this iteration
}()
}
}
Error Handling in Defer
func mightFail() (err error) {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
if err == nil {
err = closeErr // Combine errors with named return
}
}
}()
// Process file...
return nil
}
Practical Patterns
Resource Cleanup (Named Return)
func openResource() (err error) {
r, err := acquireResource()
if err != nil {
return
}
defer func() {
if closeErr := r.Close(); closeErr != nil && err == nil {
err = closeErr // Can modify named return
}
}()
// Use resource...
return nil
}
Panic Recovery
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// Code that might panic...
return nil
}
Transaction Pattern
func transaction() error {
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// Do work...
return tx.Commit().Error
}
Performance Considerations
Defer is cheap for most use cases. However, in very tight loops with millions of iterations, the overhead of managing the defer chain becomes measurable:
// Slower: defer in tight loop (millions of iterations)
func processBatch1(items []Item) {
for _, item := range items {
mu.Lock()
defer mu.Unlock() // Creates millions of deferred records
process(item)
}
}
// Faster: explicit unlock (millions of iterations)
func processBatch2(items []Item) {
for _, item := range items {
mu.Lock()
process(item)
mu.Unlock() // Immediate cleanup, no defer overhead
}
}
When to use defer:
- One-time resource cleanup (file opens, lock acquisition)
- Error handling and panic recovery
- Any non-performance-critical code
When to avoid defer:
- Extremely hot loops with millions of iterations
- Real-time systems with strict latency requirements
Modern Go (1.20+) has optimized defer significantly with open-coded defers, making it even cheaper in most scenarios.
Top comments (0)