defer
Cleanup you cannot forget, and the two traps that catch most people once.
- Predict the order in which several deferred calls run.
- Explain why
defer fmt.Println(x)prints the valuexhad on the defer line. - Fix a loop that defers
Closeso each file is closed on time.
What defer does
defer schedules a function call to run when the surrounding function returns, by whichever path, including a panic. You write the cleanup next to the thing that needs cleaning up, and it runs even if a later line returns early.
It is how Go does the job that finally does elsewhere, without the extra block.
func readFile(name string) error { f, err := os.Open(name) if err != nil { return err } defer f.Close() // runs on EVERY return path below // ... twenty lines with five different returns ... return nil}See it run
Three defers, then a normal line. Watch the order: the body finishes first, then the defers run last-in, first-out (LIFO), like taking plates off a stack.
package mainimport "fmt"func main() { defer fmt.Println("deferred 1 — runs last") defer fmt.Println("deferred 2") defer fmt.Println("deferred 3 — runs first") x := 1 defer fmt.Println("deferred x =", x) // x is read NOW, so this prints 1 x = 99 fmt.Println("main body finished, x =", x)}Trap 1 — arguments are evaluated immediately
Only the call is deferred. The arguments are evaluated when the defer line runs, which surprises most people the first time.
start := time.Now()defer fmt.Println(time.Since(start)) // prints ~0s — evaluated NOWdefer func() { fmt.Println(time.Since(start)) }() // correct: evaluated laterTrap 2 — defer runs at function end, not block end
for _, name := range files { f, _ := os.Open(name) defer f.Close() // nothing closes until the WHOLE function returns}// Over 10,000 files you run out of file handles. The fix is to move the body// into its own small function, so each iteration returns and its defer fires.Where defer earns its place
A deferred closure can change a named return value. That is how a transaction helper knows whether to commit or roll back, and how you turn a panic into an ordinary error.
In plain words: a panic is Go crashing the current function because something went badly wrong (dividing by zero, reading past the end of a list). recover() is a built-in you call inside a deferred function: if a panic is happening it stops the crash and hands you the panic value; otherwise it returns nil. You will rarely need it, but this is the one pattern to recognise.
func safe() (err error) { // err is NAMED defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered: %v", r) // the caller sees this } }() mightPanic() return nil}Close when you have been writing. For buffered writers such as gzip.Writer, Close is where the last data is flushed. For an os.File, a failed earlier write can be reported only at Close (on NFS, for example). A plain defer f.Close() throws that error away, and the end of the file with it.deferruns when the function returns, not when the block ends. LIFO order.- Arguments are evaluated immediately; only the call happens later.
deferinside a loop does not run until the whole function ends.- A deferred closure can modify a named return value. Rollback helpers rely on this.
n := 1, defer fmt.Println(n), change n to 2, and work out why it prints 1. Fix it with a deferred closure so it prints 2.- 1Open ten files in a loop with
defer f.Close()and work out when they actually close. - 2Fix that by moving the loop body into its own function.
- 3Use a deferred closure to turn a panic into a returned error.