Dev Skills

defer

Cleanup you cannot forget, and the two traps that catch most people once.

Topic 10 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Predict the order in which several deferred calls run.
  • Explain why defer fmt.Println(x) prints the value x had on the defer line.
  • Fix a loop that defers Close so 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.

real-world excerpt — opening a file (os.Open is covered in The standard library)
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
}
Deferred calls run in reverse order, last one first, like a stack.

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.

press Run — this executes on the Go Playground
package main
import "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)
}
Output: the body line with x = 99, then "deferred x = 1", then 3, 2, 1.

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.

trap1.go (excerpt — time.Now and time.Since read the clock)
start := time.Now()
defer fmt.Println(time.Since(start)) // prints ~0s — evaluated NOW
defer func() { fmt.Println(time.Since(start)) }() // correct: evaluated later

Trap 2 — defer runs at function end, not block end

trap2.go (real-world excerpt)
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.

named-return.go
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
}
Watch out: Check the error from 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.
The short version
  • defer runs when the function returns, not when the block ends. LIFO order.
  • Arguments are evaluated immediately; only the call happens later.
  • defer inside a loop does not run until the whole function ends.
  • A deferred closure can modify a named return value. Rollback helpers rely on this.
Remember: defer puts cleanup next to the thing that needs cleaning up, and runs it on every return path.
Build this before moving on
Write a function with three defers and predict their order before running it. Then set 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.
Then try these in your own editor
  1. 1Open ten files in a loop with defer f.Close() and work out when they actually close.
  2. 2Fix that by moving the loop body into its own function.
  3. 3Use a deferred closure to turn a panic into a returned error.
Check yourselfTake the Core language test