When I was first learning Go, one construct struck me as unusual: the defer statement.
Despite its apparent simplicity, it hides enough subtleties to surprise even an experienced
developer.
What defer is and why you need it#
defer postpones a function call until the surrounding function returns. It doesn’t matter how we
leave it: via return, by reaching the end of the body, or through a panic. The deferred function
runs regardless. Handy for cleaning up resources.
Where defer is useful:
- Releasing resources (closing files, database connections, HTTP response bodies)
- Synchronization (unlocking mutexes, waiting on a WaitGroup)
- Handling errors and panics
- Flushing buffered data
- Measuring function execution time for metrics
The three rules of defer#
- The arguments of a deferred call are evaluated when the
deferstatement runs, not when the call actually happens.
| |
If you want defer to see the current value, use a closure. It captures the variable by reference:
| |
- Deferred functions run in LIFO order (last in, first out).
| |
- Deferred functions can read and modify the function’s named return values.
| |
How it works: return "old" first assigns value = "old", then the deferred functions run (and may change value), and only then does the function actually return.
defer, panic, and recover#
In Go you can only recover from a panic through defer. When a function calls panic, normal execution stops. Go begins unwinding the stack: it walks up the call chain and, at each level, runs all deferred functions in LIFO order. If none of them calls recover, the program terminates and prints a stack trace.
| |
An important detail: recover only works when called directly from a deferred function. Moving the handling into a helper seems reasonable, but it won’t work:
| |
Another subtlety: a panic propagates only up the current goroutine’s stack. If a goroutine panics without recovering, the whole program crashes, not just that goroutine.
| |
Pitfalls and gotchas#
Ignoring the error#
| |
Looks correct, but there’s a catch. If f.Close() returns an error, it’s lost. To avoid losing it, make the return error named and wrap the close in a defer. Thanks to the third rule, the deferred function can read err and even change it.
| |
An alternative: call f.Sync() before returning to flush the data, and leave the deferred f.Close() to just close the descriptor.
| |
errors.Join catches every error but requires named return values. f.Sync() is simpler: no closures
and no named values. It also separates concerns: Sync() is responsible for the data, Close() for the descriptor.
defer inside a loop#
defer works at the function level, not the block level. Inside a loop, that creates a problem:
| |
defer postpones each f.Close() until concatFiles returns. That means open descriptors pile up, and
every file is closed only at the very end. Pass a slice of 1000 filenames and you’ll have 1000
descriptors open at once. The OS caps the number per process, usually 1024 by default. For a thousand files there may simply not be enough.
How to fix it:
- Extract the file reading into a separate function
- Skip
deferand callf.Close()explicitly - Use an immediately invoked anonymous function (IIFE)
Here’s the version with a separate function:
| |
Value receiver in defer#
The first rule says arguments are evaluated at the moment of defer. But a method’s receiver is an argument too, even though it doesn’t look like one. If the method takes a value receiver, the struct is copied when the defer is registered:
| |
Fix it with a pointer receiver:
| |
os.Exit()#
A deferred function does not run if the program exits via os.Exit(). This can hurt graceful
shutdown. If main() has deferred functions that close connections or flush buffers, but you exit
through os.Exit() (or log.Fatal, which calls os.Exit(1) under the hood), none of them will run.
| |
Conclusion#
defer is one of those Go mechanisms that are simple on the surface but require understanding the nuances. Remember three things: arguments are evaluated at registration, not at the call; inside loops defer accumulates resources until the function returns; and you can’t ignore the Close() error when writing. Then defer becomes a reliable tool rather than a source of surprises.