defer schedules a function call to run when the surrounding function returns — whether it returns normally, returns early via any path, or panics. It replaces try/finally.
defer vs try-finally
☕ Java — try-finally for cleanup
ResultSet rs = null;try { rs = db.query("SELECT ..."); // ... process ... return result;} finally { // runs even if exception thrown if (rs != null) rs.close();}
◎ Go — defer for cleanup
rows, err := db.Query("SELECT ...")if err != nil { return nil, err}// Register cleanup RIGHT AFTER acquiring the resource// Much harder to forget than a finally blockdefer rows.Close() // runs when queryMessage() returns// ... process rows ...return result, nil // rows.Close() runs here// Any early return above also triggers rows.Close()
Why Go does thisdefer collocates the cleanup with the acquisition. In Java, the cleanup lives in finally which may be 50 lines away. With defer, rows.Close() is on the line after rows, err := db.Query() — impossible to forget, impossible to miss in a code review.
LIFO — multiple defers stack
defers run in reverse order of registration
func openResources() { db := openDatabase() // step 1: open database defer db.Close() // registered 1st — runs LAST conn := db.NewConn() // step 2: open connection (depends on db) defer conn.Close() // registered 2nd — runs 2nd tx, _ := conn.Begin() // step 3: begin transaction (depends on conn) defer tx.Rollback() // registered 3rd — runs FIRST // If tx.Commit() succeeds, Rollback() is a safe no-op // ... do work ... tx.Commit()}// Cleanup order: tx.Rollback(), conn.Close(), db.Close()// = exact mirror of open order — each resource closed before its dependency
→LIFO mirrors the open order. Resources opened last (most dependent on others) are closed first. This is the correct order for safe cleanup.
Argument capture — evaluated at defer time
defer arguments are snapshotted when defer is registered
i := 0defer fmt.Println(i) // i evaluated NOW = 0i = 100// Function returns — deferred Println prints 0, not 100// The argument was captured at the defer line// To capture the FINAL value, use a closure (no arguments):j := 0defer func() { fmt.Println(j) // j read at exit time — prints 100}()j = 100
Tracing pattern — enter/exit timing
single-line function tracing with defer
func trace(name string) func() { start := time.Now() fmt.Printf(">>> enter %s\n", name) // Returns a cleanup function return func() { fmt.Printf("<<< exit %s (%v)\n", name, time.Since(start)) }}func (e *Engine) RouteMessage(msg Message) error { defer trace("RouteMessage")() // two calls: trace() then defer on result // ^^^^^^^^^^^^^^^^ trace() runs NOW — prints '>>> enter' // ^^ returned func is deferred — prints '<<< exit' return e.store.Save(msg)}
// panic unwinds the stack, running deferred functionsfunc riskyOp() { panic("something broke")}// recover() inside a deferred function catches a panicfunc safeOp() (err error) { defer func() { if r := recover(); r != nil { // convert panic to error — goroutine survives err = fmt.Errorf("recovered panic: %v", r) } }() riskyOp() // panics — recover catches it above return nil}
Why Go does thisPanic is not for normal error handling — it is for truly unrecoverable situations (nil pointer, out of bounds, programmer error). Use it at startup for missing required config. In production handlers, recover prevents one bad request from crashing the server.
ℹThe full defer + mutex and defer + channel patterns are in section 13 — Defer Advanced — after those primitives are introduced.