Hello! Welcome back to our Go crash course.
In the last lesson, we dove deep into Go's modern error handling, covering error wrapping with %w and how to inspect error chains using errors.Is and errors.As. You learned that robust error handling is crucial, especially in systems like Kubernetes operators where functions often have multiple return paths—one for success and several for different error conditions.
Today, we'll cover Go's defer statement. It's a simple but powerful control flow mechanism that works hand-in-hand with the error handling patterns you just learned. The primary goal of this lesson is for you to be able to use the defer statement for resource cleanup. This is a fundamental Go idiom that ensures your programs are reliable and don't leak resources like file handles or network connections, a common concern in the IoT and backend systems you've worked with.
1. What is defer?
The defer statement schedules a function call to be run immediately before the function executing the defer returns. It's a guarantee: no matter how the function exits—whether by a return statement, reaching the end of the function block, or even a panic—the deferred call will be executed.
Its most common use is to clean up resources. Imagine you open a file. You must remember to close it.
package main
import (
"fmt"
"os"
)
func main() {
// Open a file, which returns a file handle and a potential error.
f, err := os.Create("/tmp/myfile.txt")
if err != nil {
fmt.Println("Error creating file:", err)
return // Exit early on error
}
// Defer the closing of the file.
// This will run when main() is about to exit.
defer f.Close()
fmt.Println("File created. Writing to it...")
_, err = f.WriteString("Hello, Go!")
if err != nil {
fmt.Println("Error writing to file:", err)
// No need to call f.Close() here, defer handles it!
return
}
}
Without defer, you would need to call f.Close() before every return statement. This is tedious and error-prone. defer lets you place the cleanup code right next to the resource acquisition code, making your program much cleaner and more robust.

2. Core Mechanics of defer
To use defer effectively, you need to understand three key behaviors: its execution order, how its arguments are evaluated, and its interaction with return values.
For a comprehensive look at these mechanics, the following article provides clear, code-driven explanations.
How to Use defer Correctly in Go
The article 'How to Use defer Correctly in Go' from OneUptime provides an excellent overview of defer's core behaviors and common patterns.
Please read the following sections: Execution Order (LIFO): Understand how multiple deferred calls are stacked and executed. Argument Evaluation Timing: This is a critical concept. Pay close attention to when the arguments of a deferred function are evaluated. Modifying Return Values: See how a deferred function can interact with the return values of its enclosing function.
Let's summarize and reinforce those key points.
Execution Order: Last-In, First-Out (LIFO)
If a function has multiple defer statements, they are pushed onto a stack. When the function returns, the deferred calls are popped off the stack and executed in reverse order of how they were deferred.

This LIFO order is useful for nested resources. For example, if you open a database connection and then start a transaction on it, you'd want to close the transaction before you close the connection.
conn, _ := db.Open()
defer conn.Close() // Will run second
tx, _ := conn.Begin()
defer tx.Rollback() // Will run first
Argument Evaluation: Immediately
This is the most common source of bugs with defer. The arguments to a deferred function are evaluated when the defer statement is executed, not when the function call is executed.
Consider this example:
func main() {
i := 0
defer fmt.Println("Deferred print:", i) // i is 0 here
i++
fmt.Println("Current value:", i) // i is 1 here
}
// Output:
// Current value: 1
// Deferred print: 0
The value of i (which is 0) was captured when defer was called. The later i++ does not affect the argument passed to the deferred fmt.Println.
To use the current value of a variable at the time the deferred call runs, you can use a closure (an anonymous function):
func main() {
i := 0
defer func() {
// This function body runs later, but it has access
// to the 'i' variable from its surrounding scope.
fmt.Println("Deferred print:", i)
}()
i++
fmt.Println("Current value:", i)
}
// Output:
// Current value: 1
// Deferred print: 1
3. Practical Patterns for Resource Cleanup
Given your background, you'll immediately recognize these patterns. defer makes them particularly elegant in Go.
- File Handling: As we've seen,
defer file.Close(). - Mutex Locking: Essential for concurrent code. This is a pattern you'll see constantly in the Redis operator to protect shared data structures within the controller. It ensures a lock is always released.
mu.Lock() defer mu.Unlock() // Safely access shared data... - Database Transactions: Guarantees a transaction is rolled back if it isn't explicitly committed.
tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() // A successful Commit() will make this a no-op. // ... perform operations with tx ... return tx.Commit() - HTTP Requests: In client code, you must close the response body to prevent leaking connections.
resp, err := http.Get("https://example.com") if err != nil { return err } defer resp.Body.Close()
4. A Critical Pitfall: defer in a Loop
A defer statement is tied to its surrounding function, not its surrounding block (like a for loop). A common mistake is to use defer inside a loop, expecting it to run at the end of each iteration. Instead, all deferred calls will pile up and only execute when the entire function returns.
This can lead to resource exhaustion, for example, by opening many files or database connections and not closing them until the very end.
Gotchas of defer in Go | Practical Engineering
The article 'Gotchas of defer in Go' provides a fantastic, practical demonstration of why deferring in a loop is a problem and how to fix it.
Read the section 'Do not defer in loop'. The example uses a PostgreSQL server with a connection limit to concretely demonstrate how this mistake leads to a crash. Pay attention to the fix, which involves wrapping the loop body in an anonymous function.
Here is the incorrect vs. correct pattern from the article:
WRONG: Leaks resources until the function exits.
func processItems(items []Item) {
for _, item := range items {
resource, err := acquireResource(item)
if err != nil { continue }
defer resource.Close() // WRONG! This doesn't run until processItems returns.
// ... use resource ...
}
}
CORRECT: The deferred call runs at the end of each iteration.
func processItems(items []Item) {
for _, item := range items {
// We create an anonymous function and call it immediately.
func() {
resource, err := acquireResource(item)
if err != nil { return }
defer resource.Close() // CORRECT! Runs when this anonymous function returns.
// ... use resource ...
}() // The () calls the function.
}
}
Conclusion
You've now learned about defer, a key piece of Go's approach to writing clear and robust code. It's not just a convenience; it's a fundamental tool for resource management.
Key Takeaways:
deferschedules a function call to run just before the surrounding function returns.- Its primary use is for resource cleanup (closing files, unlocking mutexes, etc.).
- Multiple
defercalls execute in Last-In, First-Out (LIFO) order. - Arguments to a deferred function are evaluated immediately, which can be a "gotcha". Use a closure to capture the final value of a variable.
- Be very careful when using
deferinside a loop; you often need to wrap the loop body in a function to get the desired behavior.
This lesson concludes our "Go Language Crash Course" module. You now have a solid grasp of Go's syntax, data structures, error handling, and control flow.
Our next lesson begins a new module: Concurrency and Kubernetes Operator Foundations. We'll start with goroutines and channels, the core components of Go's powerful concurrency model. You'll see how these are the engine that drives the operator's reconciliation loop, and the defer mu.Unlock() pattern you saw today will be directly applicable.