The cleanup problem
You are writing a function that opens a file, parses a header, writes some data, and closes the file. Halfway through, the parser hits a malformed line. You return an error. The file stays open. You add a Close() call right before the return. Then you add another error check halfway down. Now you have two Close() calls. Add a third error path, and you are copy-pasting cleanup code everywhere. The code is messy, and you are about to leak resources.
Go solves this with defer. You write the cleanup once, right after you acquire the resource. The keyword defer schedules a function call to run when the surrounding function returns. It does not matter if the function returns normally, returns early due to an error, or crashes. The deferred call runs.
Defer is the standard tool for resource management. It keeps cleanup logic close to acquisition logic and eliminates duplicate code across error paths.
Defer: schedule cleanup once
Defer takes a function call and puts it on a stack. When the function containing the defer returns, Go executes the deferred calls in reverse order. This matches how resources work. You open a database, then a transaction, then a file. You close the file, then the transaction, then the database. Last in, first out.
package main
import "fmt"
// OpenResource simulates acquiring a resource that must be released.
func OpenResource(name string) {
fmt.Println("Acquired", name)
// Defer schedules the cleanup to run when OpenResource returns.
// This runs even if the function returns early due to an error.
defer fmt.Println("Released", name)
if name == "leak" {
fmt.Println("Error: cannot process leak")
return
}
fmt.Println("Working with", name)
}
func main() {
OpenResource("file.txt")
OpenResource("leak")
}
The output shows Released appearing after the function logic finishes, regardless of the early return. Defer guarantees execution.
Defer runs when the function returns. Arguments evaluate when you write the defer.
How defer works: stack and timing
The compiler transforms defer into code that pushes a function pointer and its arguments onto a per-function stack. When the function is about to return, Go pops the stack and calls each entry.
There is a subtle timing rule that catches many developers. Defer evaluates its arguments immediately, at the moment the defer statement runs. The function call itself happens later, when the surrounding function returns.
package main
import "fmt"
// Counter demonstrates that defer arguments are evaluated immediately.
func Counter() {
i := 0
// i is evaluated now. The value 0 is captured.
// The deferred function will print 0, not the final value of i.
defer fmt.Println("Deferred i:", i)
i++
fmt.Println("Current i:", i)
}
func main() {
Counter()
}
The output prints Current i: 1 then Deferred i: 0. The defer captured the value of i at the time of the defer statement. This behavior is consistent and predictable. It allows you to capture the state of variables at a specific point in time.
Most editors run gofmt on save. Trust gofmt. Argue logic, not formatting. The formatting tool ensures your defer statements are aligned and readable, but it cannot fix logic errors caused by misunderstanding argument evaluation.
Defer is cheap, but not free. The runtime maintains a list of deferred calls for each function. In a tight loop processing millions of items, the overhead of deferring a cleanup call adds up. If you are in a hot loop, call the cleanup function directly at the end of the iteration.
Defer is cheap. Don't defer in tight loops.
The loop variable trap
The immediate evaluation of arguments protects you from a common bug in loops. If you defer a function that uses a loop variable, you must capture the variable explicitly.
package main
import "fmt"
// LoopTrap demonstrates why defer arguments are evaluated immediately.
func LoopTrap() {
for i := 0; i < 3; i++ {
// i is evaluated now. All defers capture the same variable i.
// By the time they run, the loop has finished and i is 3.
defer fmt.Println(i)
}
}
func main() {
LoopTrap()
}
This prints 3, 3, 3. The defer statements capture the variable i, not its value. When the function returns, the loop has finished and i is 3. The deferred calls all print the current value of i.
To fix this, pass the value as an argument to the deferred function. The argument evaluation captures the value at that iteration.
// LoopFix captures the loop variable by passing it as an argument.
func LoopFix() {
for i := 0; i < 3; i++ {
// The argument i is evaluated immediately.
// Each defer captures the value of i for that iteration.
defer fmt.Println(i)
}
}
This prints 2, 1, 0. The values are captured correctly. The order is reversed because defer runs in LIFO order.
Defer arguments evaluate now. Defer runs later.
Panic and recover: handling the unhandled
Defer handles cleanup. Panic and recover handle catastrophic failures. A panic stops execution and starts unwinding the stack. It runs all deferred functions as it climbs up the call stack. If nothing catches the panic, the program prints a stack trace and exits.
Recover is a built-in function that stops the panic. It checks if the program is panicking. If so, it returns the panic value and restores normal flow. If not, it returns nil.
Recover only works inside a deferred function. If you call recover in normal code, it returns nil and does nothing. This design prevents accidental recovery. You must explicitly defer a function to catch a panic.
package main
import "fmt"
// SafeDivide handles division and prevents panics from crashing the program.
func SafeDivide(a, b int) int {
// Recover must be inside a deferred function to catch panics.
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from", r)
}
}()
if b == 0 {
panic("division by zero")
}
return a / b
}
func main() {
fmt.Println(SafeDivide(10, 2))
fmt.Println(SafeDivide(10, 0))
}
The output shows the normal result and the recovered message. The panic is caught, and the program continues.
Recover is a last resort. It leaves the program in an unknown state. The stack is partially unwound. Some resources might be cleaned up, others might not. Recover is useful for protecting a host program from panics in untrusted code, like a web server handling user requests. It is not a replacement for error handling.
Panic is a bug. Recover is a safety net. Fix the bug.
Realistic example: protecting a web server
Web servers run many requests concurrently. A panic in one request can crash the entire server. Middleware that recovers from panics keeps the server alive and returns a safe error response to the client.
package main
import (
"fmt"
"net/http"
)
// RecoverMiddleware wraps an HTTP handler to catch panics and return 500.
// This prevents a single bad request from crashing the entire server.
func RecoverMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Defer ensures the recovery logic runs after the handler finishes.
defer func() {
if err := recover(); err != nil {
// Log the panic details for debugging.
fmt.Printf("Panic in handler: %v\n", err)
// Send a safe error response to the client.
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next(w, r)
}
}
func main() {
http.HandleFunc("/", RecoverMiddleware(func(w http.ResponseWriter, r *http.Request) {
// Simulate a panic in the handler.
panic("unexpected nil pointer")
}))
fmt.Println("Server starting...")
http.ListenAndServe(":8080", nil)
}
The middleware defers a recovery function. If the handler panics, the deferred function catches it, logs the error, and sends a 500 response. The server continues running.
Go makes errors explicit. The boilerplate if err != nil is intentional. It forces you to handle the unhappy path. Defer helps with cleanup, but it does not replace error handling. Use errors for expected failures. Use panic for bugs.
The community treats panic as a signal for programmer error, not runtime failure. If a user provides bad input, return an error. If your code dereferences a nil pointer, that is a panic.
Use errors for flow. Use panic for crashes.
Pitfalls and compiler rules
Defer requires a function call. You cannot defer an expression or a statement. The compiler rejects invalid defer syntax.
The compiler rejects defer x++ with defer requires function call; have x++. You must defer a function invocation, not an expression. If you need to defer an operation, wrap it in a function.
// ValidDefer shows how to defer an operation using a function.
func ValidDefer() {
x := 0
// Defer requires a function call. Wrap the operation in a closure.
defer func() {
x++
}()
fmt.Println(x)
}
Recover returns nil if the program is not panicking. Calling recover outside a deferred function does nothing. This is a runtime behavior, not a compiler error. The code compiles, but the recovery logic fails.
Panic prints a stack trace. This is expensive. Do not panic in performance-critical code. Panic is for stopping the program, not for control flow.
Recovering from a panic does not restore the stack. The deferred functions have already run. The program continues from the deferred function. This can lead to subtle bugs if you assume the state is unchanged.
The worst goroutine bug is the one that never logs. If you recover from a panic, log the panic value. Otherwise, you lose the stack trace and the cause of the failure.
Recover catches the panic. Log the cause.
Decision matrix
Use defer when you need cleanup code to run regardless of how a function exits, such as closing files, unlocking mutexes, or releasing database connections.
Use panic when the program reaches an unrecoverable state, like a configuration error that makes startup impossible or an invariant violation that indicates a bug in the code.
Use recover when you are building a library or middleware that must protect the host program from panics, such as an HTTP server wrapper that catches handler panics and returns a 500 status.
Use an error return value when the failure is an expected part of the operation, such as a missing file, a network timeout, or invalid user input.