Section 09
Defer
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 block
defer 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 := 0
defer fmt.Println(i) // i evaluated NOW = 0
i = 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 := 0
defer func() {
fmt.Println(j) // j read at exit time — prints 100
}()
j = 100Tracing 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 and recover
Java — RuntimeException unwinds stack
// Unchecked exception unwinds the stack
void riskyOp() {
throw new RuntimeException("something broke");
}
try {
riskyOp();
} catch (RuntimeException e) {
// caught here
}Go — panic unwinds, recover catches
// panic unwinds the stack, running deferred functions
func riskyOp() {
panic("something broke")
}
// recover() inside a deferred function catches a panic
func 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.