Defer
Cleanup code has a habit of getting lost. You open a file at the top of a function, add three early returns over the following months, and one of them forgets to close it. Go's answer is defer: schedule the cleanup on the line right after you acquire the thing, and let the runtime guarantee it runs.
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // will run no matter how this function exitsOpen on one line, close on the next. Every future return, panic, or early exit is already handled.
The rules
Three of them, and each one has consequences worth understanding.
Deferred calls run when the function returns
Not when the block ends, not when the loop iteration ends. When the enclosing function returns.
func main() {
defer fmt.Println("third")
fmt.Println("first")
fmt.Println("second")
}
// first
// second
// thirdThey run on a panic too, which is what makes them reliable for cleanup. The only thing that skips them is os.Exit.
They run in last in, first out order
func main() {
for i := 1; i <= 3; i++ {
defer fmt.Println(i)
}
}
// 3
// 2
// 1The stack ordering is deliberate, and it is exactly right for cleanup. Resources acquired later depend on ones acquired earlier, so they must be released first.
db, _ := sql.Open(...)
defer db.Close() // released last
tx, _ := db.Begin()
defer tx.Rollback() // released first
// tx is torn down before db, which is the only sane orderArguments are evaluated immediately
This is the rule that surprises people.
func main() {
x := 10
defer fmt.Println("deferred:", x)
x = 20
fmt.Println("current:", x)
}
// current: 20
// deferred: 10x was evaluated the moment the defer statement executed, and the value 10 was stored with the scheduled call. The later assignment does not reach it.
When you want the value at exit time, wrap it in a closure:
func main() {
x := 10
defer func() {
fmt.Println("deferred:", x) // reads x when it runs
}()
x = 20
}
// deferred: 20Method calls follow the same rule for the receiver:
defer mu.Unlock() // mu is evaluated now, the call happens laterModifying named return values
A deferred closure runs after the return value has been set but before the caller sees it. If the returns are named, the closure can change them. This is the one thing that makes named returns genuinely necessary rather than merely descriptive.
func process() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
riskyOperation() // may panic
return nil
}If riskyOperation panics, the deferred function catches it and sets err. The caller receives an ordinary error instead of a crashing program.
The same technique adds context to errors on the way out:
func loadConfig(path string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("loading config %s: %w", path, err)
}
}()
// every return below gets the wrapping for free
data, err := os.ReadFile(path)
if err != nil {
return err
}
return yaml.Unmarshal(data, &cfg)
}This only works with named return values. With func process() error, the deferred function has no variable to assign to, and the returned error is whatever the return statement set. Silent no-ops from this mistake are hard to spot, so double check the signature when you use this pattern.
The loop problem
defer schedules for function exit, so putting one inside a loop piles them up:
func processAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // nothing closes until processAll returns
parse(f)
}
return nil
}Ten thousand paths means ten thousand open file descriptors, and on most systems the process hits its limit long before the loop finishes.
The standard fix is to give the body its own function, so its defers fire per iteration:
func processAll(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs at the end of this call
return parse(f)
}An anonymous function inline works too, though the named version usually reads better and is easier to test.
Handling errors from deferred calls
defer f.Close() discards whatever Close returns. For a file you only read, that is fine. For a file you wrote to, Close is where the final flush happens, and a failure there means your data did not land.
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
closeErr := f.Close()
if err == nil {
err = closeErr // report the close failure if nothing else failed
}
}()
_, err = f.Write(data)
return err
}The condition matters. If the write already failed, that error is more informative than the close error, so it wins.
A quick rule: defer f.Close() is fine for readers, and needs the error checked for writers. The same applies to anything that buffers, including bufio.Writer, gzip writers, and database transactions.
What defer costs
Very little, and less than it used to. Go 1.14 introduced open coded defers, which the compiler inlines directly into the function exit path. A defer in a straightforward function now costs a handful of nanoseconds, close to a normal function call.
The optimisation does not apply when a defer appears inside a loop or when a function has more than eight of them. Those fall back to the older, slower mechanism. This almost never matters, and it is worth knowing only so you do not avoid defer out of vague performance anxiety.
Where defer belongs
// Files
f, _ := os.Open(path)
defer f.Close()
// Mutexes
mu.Lock()
defer mu.Unlock()
// Database transactions
tx, _ := db.Begin()
defer tx.Rollback() // no-op if Commit already succeeded
// HTTP response bodies
resp, _ := http.Get(url)
defer resp.Body.Close()
// Timing
start := time.Now()
defer func() { log.Printf("took %v", time.Since(start)) }()
// Context cancellation
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()That transaction pattern is particularly neat. Rollback on an already committed transaction returns an error that you ignore, so a single deferred rollback handles every failure path while the successful path commits normally:
func transfer(db *sql.DB, from, to string, amount int64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from); err != nil {
return err
}
if _, err := tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to); err != nil {
return err
}
return tx.Commit()
}Three failure points, one line of cleanup, and no way to leave a transaction hanging.
os.Exit and log.Fatal terminate the process immediately and skip every pending defer. log.Fatal calls os.Exit(1) internally, which makes it a poor choice anywhere except the top of main. In a library or a handler, return an error instead.
The habit to form
Put the defer on the line immediately after acquiring the resource, before any code that might return. That way the cleanup and the acquisition are adjacent, and a reviewer can verify the pairing without reading the rest of the function.
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // right here, not fifteen lines downNext, let's finish this section with recursion, and the specific things Go does and does not do to support it.
How is this guide?
Last updated on
