Go Panics

Understanding and handling runtime panics in Go

💥 What are Go Panics?

Panics are runtime errors that stop normal program execution. They occur when something goes seriously wrong, like accessing invalid memory. Unlike errors, panics should be rare and indicate programming bugs rather than expected conditions.


// This will cause a panic
func causePanic() {
    var slice []int
    fmt.Println(slice[0]) // Index out of range!
}

// Recover from panic
func safePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    causePanic()
}
                                    

Output:

Recovered from panic: runtime error: index out of range [0] with length 0

Panic Concepts

Key Panic Concepts

🚨

Runtime Errors

Serious errors that stop execution

panic("something went wrong")
🛡️

Recover

Catch and handle panics gracefully

if r := recover(); r != nil {}
📚

Stack Unwinding

Deferred functions run during panic

defer cleanup()
panic("error")
🎯

Goroutine Scope

Panics affect only current goroutine

go func() {
    panic("isolated")
}()

🔹 Common Panic Causes

Understanding what typically causes panics in Go:

package main

import "fmt"

func demonstratePanics() {
    // 1. Index out of range
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic 1:", r)
        }
    }()
    
    slice := []int{1, 2, 3}
    fmt.Println(slice[10]) // This will panic
}

func nilPointerPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic 2:", r)
        }
    }()
    
    var ptr *int
    fmt.Println(*ptr) // Dereferencing nil pointer
}

func typePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic 3:", r)
        }
    }()
    
    var x interface{} = "hello"
    num := x.(int) // Type assertion failure
    fmt.Println(num)
}

func channelPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic 4:", r)
        }
    }()
    
    ch := make(chan int)
    close(ch)
    ch <- 1 // Send on closed channel
}

func main() {
    fmt.Println("Demonstrating common panics:")
    demonstratePanics()
    nilPointerPanic()
    typePanic()
    channelPanic()
    fmt.Println("All panics recovered!")
}

Output:

Demonstrating common panics:
Panic 1: runtime error: index out of range [10] with length 3
Panic 2: runtime error: invalid memory address or nil pointer dereference
Panic 3: interface conversion: interface {} is string, not int
Panic 4: send on closed channel
All panics recovered!

🔹 Panic and Recover Pattern

The proper way to handle panics with recover:

func safeFunction(data []int, index int) (result int, err error) {
    // Set up panic recovery
    defer func() {
        if r := recover(); r != nil {
            // Convert panic to error
            err = fmt.Errorf("panic occurred: %v", r)
            result = 0
        }
    }()
    
    // Potentially dangerous operation
    result = data[index] * 2
    return result, nil
}

func safeDivision(a, b float64) (result float64, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("division error: %v", r)
            result = 0
        }
    }()
    
    if b == 0 {
        panic("division by zero")
    }
    
    return a / b, nil
}

func main() {
    // Test safe function with valid input
    if result, err := safeFunction([]int{1, 2, 3, 4, 5}, 2); err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Result: %d\n", result)
    }
    
    // Test safe function with invalid input
    if result, err := safeFunction([]int{1, 2, 3}, 10); err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Result: %d\n", result)
    }
    
    // Test safe division
    if result, err := safeDivision(10, 0); err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Division result: %.2f\n", result)
    }
}

Output:

Result: 6
Error: panic occurred: runtime error: index out of range [10] with length 3
Error: division error: division by zero

🔹 When to Use Panic

Appropriate use cases for panic in Go programs:

// 1. Initialization failures
func init() {
    configFile := "app.config"
    if _, err := os.Stat(configFile); os.IsNotExist(err) {
        panic(fmt.Sprintf("Critical config file missing: %s", configFile))
    }
}

// 2. Programming errors (assertions)
func processArray(arr []int) {
    if len(arr) == 0 {
        panic("processArray called with empty array - this should never happen")
    }
    
    // Process array...
    for i, v := range arr {
        fmt.Printf("Index %d: %d\n", i, v)
    }
}

// 3. Unrecoverable state
type Database struct {
    connected bool
}

func (db *Database) Query(sql string) []string {
    if !db.connected {
        panic("database connection lost - cannot continue")
    }
    
    // Simulate query
    return []string{"result1", "result2"}
}

// 4. Library initialization
func NewCriticalService(config string) *CriticalService {
    if config == "" {
        panic("CriticalService requires non-empty config")
    }
    
    return &CriticalService{config: config}
}

type CriticalService struct {
    config string
}

func main() {
    // Good use: Programming assertion
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Caught panic: %v\n", r)
        }
    }()
    
    // This will panic because it's a programming error
    processArray([]int{}) // Empty array should never be passed
}

Output:

Caught panic: processArray called with empty array - this should never happen

🔹 Panic vs Error Guidelines

When to use panic vs regular error handling:

Use Panic When:

  • Programming bugs: Conditions that should never occur
  • Initialization failures: Critical setup that cannot continue
  • Unrecoverable state: System is in invalid state
  • Library misuse: API called incorrectly

Use Error When:

  • Expected failures: Network timeouts, file not found
  • User input errors: Invalid data from users
  • External dependencies: Database connection issues
  • Business logic: Validation failures
// Good: Use error for expected failures
func readUserFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, fmt.Errorf("failed to read user file: %w", err)
    }
    return data, nil
}

// Good: Use panic for programming errors
func calculatePercentage(part, total int) float64 {
    if total == 0 {
        panic("calculatePercentage: total cannot be zero")
    }
    return float64(part) / float64(total) * 100
}

// Bad: Don't panic for user errors
func badParseAge(input string) int {
    age, err := strconv.Atoi(input)
    if err != nil {
        panic("invalid age input") // Should return error instead
    }
    return age
}

// Good: Return error for user input
func goodParseAge(input string) (int, error) {
    age, err := strconv.Atoi(input)
    if err != nil {
        return 0, fmt.Errorf("invalid age format: %s", input)
    }
    return age, nil
}

🧠 Test Your Knowledge

When should you use panic in Go?