Don't Forget to Close the Door
Think of defer like a "cleanup note" you write at the top of a function. No matter how the function exits — normal return, early return due to an error, or even a panic — Go executes all deferred statements before the function returns, in last-in-first-out order.
This solves a real problem. Without defer, you have to remember to close files, release locks, and disconnect from databases at every possible exit point in a function. Miss one, and you have a resource leak.
package main
import (
"fmt"
"os"
)
func processConfig(filename string) error {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("open config: %w", err)
}
defer f.Close() // Guaranteed to run when processConfig returns
// Parse the file...
// If parsing fails and we return early, f.Close() still runs.
// If parsing succeeds and we return nil, f.Close() still runs.
fmt.Println("Config processed successfully")
return nil
}
Multiple defers execute in reverse order (LIFO)
func example() {
defer fmt.Println("third") // runs last
defer fmt.Println("second") // runs second
defer fmt.Println("first") // runs first
fmt.Println("function body")
}
// Output:
// function body
// first
// second
// third
This LIFO ordering is intentional — it mirrors the natural "undo" order of setup operations. If you open a database connection and then start a transaction, you want to roll back the transaction before closing the connection.
defer with cancel() — the most common Ops pattern
Every time you create a context.WithTimeout or context.WithCancel, you must call the returned cancel function to release the timer and associated resources. The idiomatic pattern is:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Always on the very next line
If you forget defer cancel(), the context's internal timer goroutine leaks until the parent context is cancelled. In a long-running service that creates many contexts, this accumulates into a goroutine leak — a subtle but serious production issue.