Understanding Defer, Panic, and Recover in Go Programming
Go provides powerful mechanisms for handling cleanup operations and runtime errors through defer
, panic
, and recover
. This guide covers these essential concepts in detail.
Defer Statement
Basic Defer Usage
// Basic defer example
func readFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Will be called when function returns
// Read file contents...
return nil
}
Multiple Defers
func multipleDefers() {
fmt.Println("Start")
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("End")
}
// Output:
// Start
// End
// Third defer
// Second defer
// First defer
Defer with Arguments
func deferWithArgs() {
i := 0
defer fmt.Println("Deferred value:", i)
i = 1
fmt.Println("Final value:", i)
}
// Output:
// Final value: 1
// Deferred value: 0
Panic
Basic Panic Usage
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
func main() {
result := divide(10, 0) // This will panic
fmt.Println(result) // Never reached
}
Custom Panic Values
type CustomError struct {
Code int
Message string
}
func processData(data []byte) {
if len(data) == 0 {
panic(CustomError{
Code: 500,
Message: "empty data",
})
}
// Process data...
}
Recover
Basic Recovery
func recoverExample() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
panic("something went wrong")
fmt.Println("This line never executes")
}
Selective Recovery
func selectiveRecover() {
defer func() {
if r := recover(); r != nil {
switch x := r.(type) {
case string:
fmt.Printf("Recovered string panic: %s\n", x)
case error:
fmt.Printf("Recovered error panic: %v\n", x)
default:
panic(r) // Re-panic for unknown types
}
}
}()
// Your code here...
}
Best Practices
1. Resource Cleanup
// Good: Proper resource cleanup
func processFiles(filenames []string) error {
for _, filename := range filenames {
if err := processFile(filename); err != nil {
return fmt.Errorf("processing %s: %w", filename, err)
}
}
return nil
}
func processFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Always closes file
// Process file...
return nil
}
2. Database Transactions
func performTransaction(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r) // Re-panic after rollback
}
}()
// Perform transaction operations...
if err := tx.Commit(); err != nil {
tx.Rollback()
return err
}
return nil
}
3. HTTP Response Handling
func handleRequest(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("Handler panic: %v", r)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}()
// Handle request...
}
Common Patterns
1. Cleanup Pattern
type Resource struct {
name string
}
func (r *Resource) Close() error {
fmt.Printf("Cleaning up resource: %s\n", r.name)
return nil
}
func useResource(name string) error {
r := &Resource{name: name}
defer r.Close()
// Use resource...
return nil
}
2. Mutex Locking
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
3. Time Tracking
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
func longOperation() {
defer timeTrack(time.Now(), "longOperation")
// Perform operation...
time.Sleep(time.Second)
}
Performance Considerations
1. Defer Overhead
// Expensive: Defer in tight loop
func badExample() {
for i := 0; i < 1000000; i++ {
f, _ := os.Open("file.txt")
defer f.Close() // Defers pile up
}
}
// Better: Close explicitly in loop
func goodExample() {
for i := 0; i < 1000000; i++ {
f, _ := os.Open("file.txt")
// Process file
f.Close()
}
}
2. Panic Cost
// Expensive: Using panic for control flow
func findIndex(slice []int, val int) int {
for i, v := range slice {
if v == val {
panic(i) // Don't do this
}
}
return -1
}
// Better: Return value directly
func findIndexBetter(slice []int, val int) int {
for i, v := range slice {
if v == val {
return i
}
}
return -1
}
Common Mistakes
1. Defer in Wrong Scope
// Wrong: Deferred close after error check
func wrongScope(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Never executed if error
return nil
}
// Right: Defer immediately after successful resource acquisition
func rightScope(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Will always execute
return nil
}
2. Recover Outside Deferred Function
// Wrong: Recover outside defer
func wrongRecover() {
if r := recover(); r != nil { // Won't work
fmt.Println("Recovered:", r)
}
panic("help!")
}
// Right: Recover inside deferred function
func rightRecover() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("help!")
}
3. Ignoring Defer Errors
// Wrong: Ignoring defer errors
func wrongErrorHandling() error {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // Error ignored
return nil
}
// Right: Handling defer errors
func rightErrorHandling() (err error) {
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
if err == nil {
err = closeErr
}
}
}()
return nil
}
Next Steps
- Learn about error handling
- Explore goroutines
- Study testing
- Practice with best practices