7.7 defer: Scheduling a Call for Function Exit
Right, let’s talk about defer. This is one of Go’s genuinely clever features, but it’s also one that can trip you up if you don’t understand its mechanics. The core idea is simple: you schedule a function call to be executed right before the surrounding function returns. It’s like saying, “Hey, on your way out, no matter how you leave, don’t forget to take out the trash.”
Why is this so brilliant? Because it brings the cleanup code right next to the setup code. You open a file, and on the very next line, you defer its closure. You acquire a mutex, and you immediately defer its unlocking. This pairing dramatically reduces the chances of you forgetting to clean up resources, especially when you have multiple return paths or panics. It’s your automatic, “do this last” ticket.
Here’s the classic, textbook example:
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close() // This will run when ReadFile returns.
return io.ReadAll(f)
}
Even if io.ReadAll fails and we return an error, f.Close() is guaranteed to run. This is infinitely cleaner than remembering to close the file before every single return nil, err statement scattered throughout your function.
The Order of Operations: LIFO (Last-In, First-Out)
This is the part that gets people. If you defer multiple calls, they don’t execute in the order you wrote them. They execute in reverse order. Think of it as a stack of plates: the last plate you put on top is the first one you take off.
func main() {
defer fmt.Println("I run first (on the way out)!")
defer fmt.Println("I run second!")
defer fmt.Println("I run last (but scheduled first)!")
fmt.Println("Function is starting...")
}
This will output:
Function is starting...
I run last (but scheduled first)!
I run second!
I run first (on the way out)!
Why on earth would they design it this way? It’s not just to be quirky. It makes perfect sense: your later defers often depend on resources set up by your earlier ones. You defer a log line that says “operation successful,” and then you defer the actual cleanup of the operation. You want the log to happen after the cleanup? Of course not. You want the log to be the very last thing that happens. LIFO ordering ensures that the most contextually important “final” action (the one you thought of last) happens last.
Arguments Are Evaluated Immediately (The Gotcha)
Here’s the big one, the foot-gun waiting to happen. The arguments to a deferred function are evaluated at the time the defer statement is executed, not when the deferred function actually runs later.
Watch this:
func main() {
start := time.Now()
defer fmt.Printf("Function took: %v\n", time.Since(start)) // Wrong!
time.Sleep(2 * time.Second)
fmt.Println("Work done.")
}
You’d expect this to print “Function took: ~2s”, but it won’t. It will print “Function took: ~0s”. Why? Because the time.Since(start) argument is evaluated immediately after start is declared, when the delay is practically zero. The Printf call is deferred, but its arguments are already locked and loaded.
The correct way is to defer a closure (a function literal) that evaluates the argument when it runs:
func main() {
start := time.Now()
defer func() { // This anonymous function has no arguments.
fmt.Printf("Function took: %v\n", time.Since(start)) // Evaluated now!
}()
time.Sleep(2 * time.Second)
fmt.Println("Work done.")
}
Now it works. The closure has access to the start variable, and time.Since(start) isn’t calculated until the closure executes on the function’s exit. This is a crucial distinction. If you’re deferring a function with arguments, remember: it’s taking a snapshot of the world right now.
Using Defer with Named Return Values
This is a powerful, slightly advanced trick. Because a deferred function is called within the scope of the exiting function, it can read and modify named return values.
func GiveMeAName() (result string) {
result = "Original"
defer func() {
if result == "Original" {
result = "Changed by the defer!" // We can modify the return value!
}
}()
return result // This sets the return value to "Original"
}
fmt.Println(GiveMeAName()) // Prints: "Changed by the defer!"
The return result statement sets the return value result to “Original”. Then, the deferred function runs, sees this value, and changes it to “Changed by the defer!”. This is the final value that is returned to the caller.
This is incredibly useful for providing context to error returns. You can defer a function that checks if the named error return is nil and, if it’s not, wraps it with more information about what the function was doing when it failed.
The One Thing Defer Can’t Handle: os.Exit
This is the final boss of defer behavior. defer schedules calls for when a function returns. os.Exit doesn’t return; it terminates the program immediately, with extreme prejudice. The runtime doesn’t get a chance to unwind the stack and run any deferred functions.
func main() {
defer fmt.Println("This will not print. At all.")
os.Exit(0) // Goodbye, cruel world.
}
It’s a harsh reality. If you need guaranteed cleanup that must run even on a forced exit, you might need to coordinate with signal handling, but that’s a topic for another chapter. For now, just know that defer is powerful, but it’s not magic. It plays by the rules of the runtime, and os.Exit breaks those rules entirely.