How to fix resource leaks and master Go's "Proximity Rule"
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."
The Proximity Rule
"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
The Stack (LIFO)
"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 1st)
- Pop File Close (Runs 2nd)
"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."
The Argument Trap
"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."
"How do I fix it?"
"Use an anonymous function to delay the execution."
defer func() {
fmt.Println("Time elapsed:", time.Since(start))
}()
Ethan ran it again.
Time elapsed: 2s
"Now the calculation happens inside the function, at the very end."
The Safety Net
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."
Key Concepts
The defer Statement:
Schedules a function call to be run immediately before the surrounding function returns.
-
Use Case: Closing files, releasing locks (
mu.Unlock), closing database connections.
Execution Order (LIFO):
Deferred calls are executed in Last In, First Out order.
- The last thing you deferred is the first thing to run.
-
Example: If you
deferclosing a file and thendeferclosing a database, the database closes first.
Panic Safety:
Deferred functions run even if the program panics. This makes them essential for recovering from crashes or ensuring resources are released during a failure.
Argument Evaluation:
-
Arguments to the deferred function are evaluated immediately (when the
deferline is hit). - The Function Body is executed later (when the function returns).
-
Tip: If you need to calculate something at the end (like timing execution), wrap it in an anonymous
func() { ... }.
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).
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (13)
Your articles are always fascinating. I genuinely prefer learning through engaging narratives like yours rather than through dry tutorials — so thank you for that, Aaron.
Just a simple question, more out of curiosity than anything else: have you ever considered grouping your Go, JavaScript, and other pieces into structured series instead of keeping them standalone? I feel like they already form a kind of narrative universe, and seeing them organized as series could make them even more powerful for readers following along.
i'll check that out! thank you Pascal 🙏❤
This is a great explanation of
defer👍I really like the proximity rule framing — opening a resource and immediately deferring its cleanup makes the code much safer and easier to reason about. The LIFO example and the argument-evaluation gotcha are especially helpful for people who think they already “know” defer but still get bitten by it.
❤🙏
“Absolutely loved this deep dive into Go’s defer statement! Who knew that a tiny little keyword could have such a big impact on keeping your code clean and bug-free? The article really demystified the subtle behaviors of defer—especially the order of execution—which I’ve sometimes underestimated in my own projects. The examples were super practical and made me appreciate how defer can save us from common mistakes like forgetting to close files or unlock mutexes. After reading this, I’m definitely going to be more mindful about where and how I use defer. A must-read for anyone who wants to write Go code that’s both elegant and robust!”
🙏❤️
Great read! 🙌 I love how this article breaks down Go’s defer in a clear and practical way — it’s one of those features that seems simple but can be surprisingly powerful (and tricky) in real code. Thanks for the helpful insights!
🙏❤✨
nice! Go looks much easier than javascript?
Hi Benjamin, yep Go is definitely simpler by design, which makes it feel much less cluttered! While JavaScript is still the king of the browser, Go shines on the backend where you need raw speed and reliability. Cheers! ✨❤
Nice! I need to take some time to learn GO. I don't use node.js for the backend anymore.
That was a great way of explaining defer in Go, learnt new things for sure, thanks mate!!
🙏❤️🌹