The cleanup that never misses
You open a file to write a log entry. Halfway through processing, a network request fails. You return an error early. The file handle leaks. You lock a mutex to protect a shared counter. An index out of bounds panic crashes the goroutine. The mutex stays locked forever. The next goroutine hangs waiting for a lock that will never be released.
Go provides defer to handle cleanup that must happen no matter how the function exits. A deferred function runs when the surrounding function returns, whether that return is normal, early, or caused by a panic. The calls stack up and execute in Last-In-First-Out order. The last deferred call runs first. This matches the natural pattern of resource management: you release the last resource you acquired before releasing the earlier ones.
How defer stacks up
defer schedules a function call to run later. The "later" is immediately before the surrounding function returns. When the compiler sees a defer statement, it generates code to push the call onto a hidden stack associated with the function frame. The arguments to the deferred call are evaluated right now, at the defer line. The function value is stored. When the function reaches a return statement or hits the closing brace, the runtime pops the stack and calls the deferred functions in reverse order.
This evaluation timing is the source of most confusion. The arguments are frozen at the moment of the defer. If you pass a variable, the deferred function sees the value that variable held when the defer executed, not the value it holds when the deferred function finally runs.
package main
import "fmt"
// Main demonstrates defer order and argument evaluation.
func main() {
i := 0
// Defer captures the current value of i.
// The argument i is evaluated immediately as 0.
defer fmt.Println("First defer, i is", i)
i++
// Defer captures the new value of i.
// The argument i is evaluated immediately as 1.
defer fmt.Println("Second defer, i is", i)
// Output:
// Second defer, i is 1
// First defer, i is 0
}
The output shows the second defer running first. The values are 1 and 0 because the arguments were evaluated at the defer lines. The first defer saw i as 0. The second defer saw i as 1. The stack unwinds, running the second call, then the first.
Defer is cheap but not free. The runtime maintains a linked list of deferred calls per goroutine. Pushing and popping this list adds a small overhead. In a tight loop processing millions of items, explicit cleanup can be faster. For typical application code, the overhead is negligible.
Defer is a stack. Arguments are frozen at the call site.
Real-world cleanup patterns
The most common use of defer is resource cleanup. Files, network connections, database transactions, and mutexes all need release logic. defer ensures that logic runs even if you add a new error check later and forget to update the cleanup path.
package main
import (
"fmt"
"os"
)
// ProcessFile reads a file and handles cleanup safely.
// It demonstrates the standard defer pattern for resource management.
func ProcessFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
// Defer closes the file handle when the function returns.
// This prevents resource leaks even if an error occurs later.
defer f.Close()
// Simulate work that might fail.
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading %s: %w", path, err)
}
fmt.Printf("Read %d bytes\n", len(data))
return nil
}
The defer f.Close() line runs immediately after the successful open. It schedules the close for later. If ReadFile fails, the function returns an error. The runtime executes the deferred close before the return completes. The file handle is released.
Sometimes you need to capture the error from a cleanup operation. defer f.Close() discards the error. If the close fails, you might want to log it. A closure lets you capture the error and handle it.
package main
import (
"fmt"
"os"
)
// SafeProcessFile demonstrates capturing cleanup errors.
// It uses a closure to log errors from Close.
func SafeProcessFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
// Defer a closure to capture the error from Close.
// This ensures the error is logged even if the function returns early.
defer func() {
if err := f.Close(); err != nil {
fmt.Println("Error closing file:", err)
}
}()
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading %s: %w", path, err)
}
fmt.Printf("Read %d bytes\n", len(data))
return nil
}
The closure captures f by reference. When the deferred function runs, it calls Close and checks the error. This pattern is useful when cleanup failures matter. The community convention is to keep defer simple. If the cleanup logic grows complex, consider extracting it into a named function rather than embedding a large closure.
Defer can also modify named return values. This is a feature that enables concise error handling, though it can reduce readability if overused.
package main
import "fmt"
// CountLines returns the number of lines in a file.
// It uses named return values to demonstrate defer modification.
func CountLines(path string) (count int, err error) {
// Named returns allow defer to modify the return values.
// This is useful for updating counters or errors during cleanup.
defer func() {
fmt.Println("Final count before return:", count)
// Defer can update named returns.
// This runs before the function returns to the caller.
if err != nil {
fmt.Println("Error occurred, count is", count)
}
}()
// Simulate work.
count = 42
return
}
The deferred function runs before the function returns. It can read and write the named return variables. The caller sees the modified values. This pattern is common in functions that need to log metrics or adjust return values based on cleanup state.
The compiler rejects attempts to modify anonymous return values from a defer. If the function signature uses anonymous returns, the deferred function cannot assign to them. The compiler complains with cannot assign to anonymous return value. Use named returns if you need defer to modify the output.
Defer runs. Always. Even on panic.
Pitfalls and compiler guards
The loop variable capture is a classic trap. If you defer a closure inside a loop, the closure captures the loop variable by reference. All deferred calls share the same variable. By the time the function returns, the loop has finished, and the variable holds its final value.
package main
import "fmt"
// LoopDefer demonstrates the loop variable capture issue.
// This code fails to compile in Go 1.22 and later.
func LoopDefer() {
for i := 0; i < 3; i++ {
// Defer a closure that uses the loop variable.
// In Go 1.22+, this is a compile error.
defer func() {
fmt.Println(i)
}()
}
}
Go 1.22 changed loop variable semantics to fix this class of bug. The compiler now rejects this code with loop variable i captured by func literal. You must capture the variable explicitly to create a new instance for each iteration.
package main
import "fmt"
// SafeLoopDefer demonstrates capturing loop variables correctly.
// It creates a new variable for each iteration.
func SafeLoopDefer() {
for i := 0; i < 3; i++ {
// Capture the loop variable in a new variable.
// This ensures each closure gets its own copy.
i := i
defer func() {
fmt.Println(i)
}()
}
// Output: 2, 1, 0
}
The i := i line creates a new variable scoped to the loop body. The closure captures this new variable. Each iteration gets a distinct variable. The deferred calls print the expected values.
Defer also interacts with panics. A deferred function runs even if the function panics. This is the only way to catch a panic and recover. The recover function stops the panic and returns the panic value. It only works inside a deferred function. Calling recover outside a defer returns nil.
package main
import "fmt"
// RecoverPanic demonstrates using defer to catch a panic.
// It shows how recover stops the panic propagation.
func RecoverPanic() {
defer func() {
// Recover stops the panic and returns the value.
// This only works inside a deferred function.
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
// This panic is caught by the deferred recover.
panic("something went wrong")
}
The deferred function runs when the panic occurs. recover intercepts the panic. The function returns normally instead of crashing. The community convention is to recover only at system boundaries. Let panics crash the goroutine in application logic. Use errors for control flow. Recover is for preventing a single bug from taking down the entire process.
Defer has a small performance cost. The runtime must manage the defer stack. In a tight loop, deferring a function for every iteration can be slower than calling the function explicitly. Benchmark your code if performance matters. For most use cases, the cost is invisible.
The compiler catches loop variable captures. Trust the error.
When to use defer
Use defer when you need guaranteed cleanup like closing files, unlocking mutexes, or flushing buffers. Use defer when a function has multiple exit points and you want to avoid duplicating cleanup code. Use defer with a closure when the cleanup logic needs to capture local variables that change after the defer statement. Use defer with recover when you are at a system boundary and need to prevent a panic from crashing the process. Use explicit cleanup code when you are in a performance-critical tight loop and the overhead of defer matters. Use named return values with defer when you need to modify the return value during cleanup, though this pattern reduces readability and should be used sparingly.
Defer is for cleanup, not control flow.