The Secret Life of Go: The 'defer' Statement
Source: Dev.to
Chapter 20: The Stacked Deck
The fan on Ethan’s desktop PC was spinning loudly. He was staring at a terminal that was spewing error messages like a broken fire hydrant.
panic: too many open files
panic: too many open files
“I don’t get it,” he muttered, hitting Ctrl+C to kill the server. “I’m closing everything. I checked three times.”
Eleanor walked by, carrying a tray of tea. “Show me the ExportData function.”
Ethan pulled up the code. It was a long function, maybe 50 lines, that opened a file, queried a database, wrote to a CSV, and then uploaded it to S3.
func ExportData(id string) error {
f, err := os.Open("data.csv")
if err != nil {
return err
}
db, err := sql.Open("postgres", "...")
if err != nil {
return err // <--- The Leak
}
// ... 40 lines of logic ...
db.Close()
f.Close() // <--- The Cleanup
return nil
}
“I close the file right there at the bottom,” Ethan pointed.
“And what happens if the database connection fails?” Eleanor asked gently.
Ethan looked at the second if err != nil block. “It returns the error.”
“And does it close the file before returning?”
Ethan froze. “No. It returns immediately. The file stays open.”
“Exactly. And if you have an error in the middle of your 40 lines of logic? The file stays open. If the function panics? The file stays open. You have created a leak.”
“In other languages,” Eleanor explained, “you might wrap this in a try...finally block. You put the cleanup way down at the bottom, far away from where you opened the resource. You have to remember to scroll down and check it.”
“In Go, we prefer Proximity.”
She took the keyboard and moved the cleanup code.
func ExportData(id string) error {
f, err := os.Open("data.csv")
if err != nil {
return err
}
defer f.Close() // Scheduled immediately!
db, err := sql.Open("postgres", "...")
if err != nil {
// f.Close() happens automatically here
return err
}
defer db.Close() // Scheduled immediately!
// ... 40 lines of logic ...
return nil
// db.Close() happens automatically here
// f.Close() happens automatically here
}
“The defer keyword pushes a function call onto a stack,” Eleanor said. “It says: ‘I don’t care how this function ends—return, error, or panic—run this code right before you leave.’”
“So I put the cleanup right next to the creation?”
“Always. Open the door, then immediately tell the door to shut itself when you leave. You will never forget again. It works for files, database connections, and especially mutex locks.”
mu.Lock()
defer mu.Unlock() // The lock is released no matter what happens
“Wait,” Ethan said, looking at the code. “I have two defers now. Which one runs first?”
“It is a stack,” Eleanor replied. “Last In, First Out.”
She sketched it on a notepad:
- Open File → push
f.Close - Open DB → push
db.Close - Function ends → pop
db.Close(runs first) → popf.Close(runs second)
“This is critical,” she noted. “Imagine you are writing a buffered writer. You need to Flush the buffer before you Close the file. Since you create the File first and the Writer second, the Writer closes first. The dependency order is handled naturally.”
Argument Evaluation Gotcha
“One warning,” Eleanor added, holding up a finger. “The function call is scheduled for later, but the arguments are evaluated now.”
“What does that mean?”
“Look at this.”
func TrackTime() {
start := time.Now()
defer fmt.Println("Time elapsed:", time.Since(start))
time.Sleep(2 * time.Second)
}
Ethan ran the code.
Time elapsed: 0s
“Zero?” Ethan asked. “But it slept for two seconds.”
“Because time.Since(start) was calculated when you wrote the defer line,” Eleanor explained. “At that exact moment, start was now, so the difference was zero.”
Fixing the Timing Example
func TrackTime() {
start := time.Now()
defer func() {
fmt.Println("Time elapsed:", time.Since(start))
}()
time.Sleep(2 * time.Second)
}
Running it again prints:
Time elapsed: 2s
Now the calculation happens inside the anonymous function, at the very end.
Ethan looked at his ExportData function. It looked safer. Robust.
“I used to think defer was just syntax sugar,” he said.
“It is a safety net,” Eleanor corrected. “We are humans. We forget things. We get distracted. defer allows you to clean up as you go, so you never leave a mess for your future self.”
The defer Statement
Use Cases
- Closing files (
f.Close()) - Releasing mutex locks (
mu.Unlock()) - Closing database connections (
db.Close())
Execution Order (LIFO)
- Deferred calls are stored on a stack.
- The last deferred function is executed first when the surrounding function returns.
Panic Safety
- Deferred functions run even if the function panics, providing a reliable cleanup path.
Argument Evaluation
- Arguments to the deferred function are evaluated immediately when the
deferstatement is reached. - The function body runs later, at return time.
Tip
If you need to compute something at the end of a function (e.g., timing), wrap the logic in an anonymous function:
defer func() {
// computation that should happen just before the function exits
}()
Next chapter: The Panic and Recover. Ethan learns that sometimes, the only way to save a program is to let it crash (and then catch it).