11. `defer`: cleanup that always runs
defer is a small Go keyword with an outsized impact on how clean Go code
reads. It schedules a function call to run when the surrounding function
is about to return — no matter how it returns.
The idea
package main
import "fmt"
func main() {
defer fmt.Println("...world")
fmt.Println("hello...")
}
Run it: hello... prints first, then ...world. The deferred call was
scheduled on the defer line but executed at the very end of main.
Why it matters
The point is putting cleanup right next to the setup it undoes. In real code
you open something — a file, a network connection, a lock — and you must
close it later. defer lets you write the close immediately, so you can't
forget it and it survives every exit path:
func readConfig() error {
f, err := os.Open("config.yaml")
if err != nil {
return err
}
defer f.Close() // guaranteed to run, however this function returns
// ... lots of code with many possible `return`s ...
// you never have to remember to close f again
return nil
}
(That box is reference-only — it touches the filesystem, which the in-browser
runner can't do. The behavior is what matters: f.Close() runs on every
return below it.)
Deferred calls run in LIFO order
Defer several calls and they run in reverse order — last deferred, first to run. Think of stacking plates: the last one you put on is the first you take off.
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("1 (deferred first)")
defer fmt.Println("2")
defer fmt.Println("3 (deferred last)")
fmt.Println("end")
}
The output is start, end, 3, 2, 1. Reverse order is exactly what
you want for cleanup: you tear down in the opposite order you set up.
Arguments are evaluated when defer runs, not later
A subtle, important detail: the arguments to a deferred call are captured
at the moment you write defer, even though the call itself happens later.
func main() {
x := 1
defer fmt.Println("deferred sees x =", x) // captures 1 right now
x = 99
fmt.Println("final x =", x)
}
// final x = 99
// deferred sees x = 1
The deferred line prints 1, not 99 — the value of x was frozen at the
defer statement. (If you need it to see the final value, defer a closure
instead — a technique we'll cover with functions.)
That box is reference-only, and for an unusually interesting reason: this is
the one place where the interpreter running these lessons disagrees with
real Go. It evaluates the arguments late and prints 99. Compiled Go
freezes them at the defer, exactly as described above — so trust the
comment, not an experiment, on this one.
The everyday pattern
You'll see this shape constantly in Go: acquire, defer the release, then do the work in between.
package main
import "fmt"
func process(name string) {
fmt.Printf("→ opening %s\n", name)
defer fmt.Printf("← closing %s\n", name)
fmt.Printf(" working with %s\n", name)
}
func main() {
process("database")
process("cache")
}
Each process call cleanly opens, works, and closes — and the close is
never more than one line away from the open.
Your turn
Complete the program so it prints exactly:
task started
task finished
cleaning up
Use a single defer for the cleaning up line, placed at the top of
main, so it runs last automatically.
package main
import "fmt"
func main() {
// defer the "cleaning up" line here
fmt.Println("task started")
fmt.Println("task finished")
}
package main
import "fmt"
func main() {
defer fmt.Println("cleaning up")
fmt.Println("task started")
fmt.Println("task finished")
}
That wraps up control flow. You can now branch, loop, and clean up — the backbone of every program. Next module: functions, where Go's real character starts to show.