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