Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
27: Defer, Panic, and Recover
In most languages, when you open a resource—a file, a database connection, a network socket—you're taught to wrap everything in a try...finally block. It's clunky, it indents your code halfway across the screen, and it separates the "open" logic from the "close" logic by twenty lines of code. In Go, we handle this differently using defer, and when things truly go sideways, we have panic and recover.
The manual cleanup dance
Let's look at how a beginner might handle writing a small configuration file. You open the file, do some work, and close it at the end. It looks clean at first glance:
func saveConfig(filename string, data []byte) error {
f, err := os.Create(filename)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
// Oh wait, I have to close the file here too!
f.Close()
return err
}
return f.Close()
}
The problem here is that as your function grows, the manual cleanup becomes a nightmare. If you have three different error checks, you're calling f.Close() in three different places. I've seen production bugs where a developer added a new if err != nil return and simply forgot to close the resource, leading to a file descriptor leak that crashed the server three days later. It's fragile.
Insurance against early exits
This is where defer comes in. Think of it as a way to schedule a function call to happen the moment the surrounding function returns, regardless of which path it took to get there. I always tell people to put the defer immediately after the resource is successfully acquired.
func saveConfig(filename string, data []byte) error {
f, err := os.Create(filename)
if err != nil {
return err
}
// Schedule this now; it will run when saveConfig returns
defer f.Close()
_, err = f.Write(data)
if err != nil {
return err // f.Close() happens automatically here
}
return nil // f.Close() happens automatically here too
}
Now the "open" and "close" logic are right next to each other. You don't have to hunt through the function to make sure every exit path is covered. One thing to keep in mind: if you have multiple defer statements, they execute in Last-In-First-Out (LIFO) order. It's like a stack; the last thing you deferred is the first thing that gets cleaned up.
When errors aren't enough
Now, we need to talk about panic. In Go, we use error for things that are expected to happen (like a file not existing). A panic is for things that should never happen—like accessing an array index that doesn't exist or a nil pointer dereference. It's the "the world is ending" button.
If you're writing a library, you should almost never call panic. It's aggressive. But if you're writing a complex parser or a compiler where a certain state is logically impossible, a panic can be a way to fail fast during development. The catch is that a panic will crash your entire program unless you catch it.
Stopping the bleed with recover
You can stop a panic from crashing your app using recover. However, recover only works inside a deferred function. If you call it in the main flow of your code, it does nothing.
Imagine you're running a web server that processes user-uploaded JSON. One specific edge case in your parser causes a panic. You don't want one bad upload to take down the entire server for every other user. You can wrap the risky logic like this:
func handleRequest(data []byte) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from a critical error: %v\n", r)
// Here you'd log the stack trace and send a 500 error to the user
}
}()
// This function might panic if the data is maliciously malformed
processRiskyData(data)
}
By deferring a function that calls recover, you're essentially creating a safety net. The panic starts climbing up the call stack, hits the deferred function, and is caught. The program stays alive, and you've turned a catastrophic crash into a handled error. Use this sparingly, though. If you use recover to hide bugs instead of fixing them, you're just building a house on a foundation of ghosts.
📋 Practical Task
Exercise: The Robust Log Processor
You are building a system that processes a batch of log files. Some files are corrupted and cause the processing logic to panic. Your goal is to ensure that one corrupted file doesn't stop the entire batch from being processed, and that every file is closed properly regardless of whether it succeeded or crashed.
Requirements:
- Create a function
processFile(name string)that simulates opening a file. - Inside
processFile, usedeferto print"Closing file: [name]". - Simulate a crash: if the filename is
"corrupt.log", callpanic("corrupt file detected!"). - Create a main loop that iterates through a slice of filenames:
[]string{"app.log", "corrupt.log", "system.log"}. - Wrap the call to
processFilein a way that recovers from the panic, prints the error message, and allows the loop to continue to the next file.
Expected Output: The program should process "app.log", hit the panic on "corrupt.log" (but not crash), and successfully process "system.log", with all "Closing file" messages printing for every file attempted.
There are no comments for now.