Introduction
Go panic and runtime errors occur when the Go runtime encounters an unrecoverable error such as nil pointer dereference, array index out of bounds, division by zero, or failed type assertions. When a panic occurs, the program stops normal execution, runs any deferred functions, prints a stack trace, and exits. While panics indicate programming errors that should be fixed, understanding how to debug them, recover gracefully where appropriate, and prevent them in production is essential for building robust Go applications. Common causes include dereferencing nil pointers, accessing slice/map/array with invalid index, type assertion on interface with wrong type, division by zero, calling methods on nil receivers, concurrent map writes, stack overflow from infinite recursion, and calling panic() explicitly for unrecoverable errors. The fix requires understanding Go's panic/recover mechanism, proper error handling patterns, defensive programming techniques, and production debugging with stack traces and profiling tools. This guide provides production-proven techniques for debugging and preventing panics across Go applications.
Symptoms
panic: runtime error: invalid memory address or nil pointer dereferencepanic: runtime error: index out of range [5] with length 3panic: runtime error: division by zeropanic: interface conversion: interface {} is int, not stringpanic: send on closed channelpanic: close of closed channel- Stack trace showing panic location and call stack
- Application exits with non-zero status code
- Deferred functions run before exit
fatal error: concurrent map writespanic: goroutine acquired a lock that it already holds
Common Causes
- Dereferencing nil pointer or interface
- Slice/array access with index >= length
- Map access with nil map (returns zero value, doesn't panic)
- Concurrent map write (panic in Go 1.6+)
- Type assertion without checking ok result
- Division or modulo by zero
- Method call on nil struct pointer
- Recursive function without proper base case
- Channel operations on nil or closed channel
- Explicit panic() call for error conditions
Step-by-Step Fix
### 1. Understand panic stack traces
Read panic output:
``` panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1234567]
goroutine 1 [running]: example.com/mypackage.(*MyStruct).MyMethod(0x0, {0xabcdef, 0x5}) /path/to/file.go:42 +0x27 example.com/mypackage.MainFunction() /path/to/main.go:15 +0x89 main.main() /path/to/main.go:8 +0x45 ```
Key information: - Panic type and message (nil pointer dereference) - Signal type (SIGSEGV = segmentation fault) - Memory address (addr=0x0 = nil) - Program counter (pc=0x1234567) - Goroutine ID and state - Call stack with file, line, and offset
Enable more detailed traces:
```bash # Get full stack trace for all goroutines kill -ABRT <pid>
# Or use debug.SetPanicOnFault for detailed memory info export GODEBUG=panicnil=1 # More info on nil panics export GODEBUG=tracebackancestors=10 # Show ancestor goroutines
# Save crash output ./app 2>&1 | tee crash.log ```
### 2. Fix nil pointer dereference
Nil struct pointer:
```go // WRONG: Nil pointer method call type User struct { Name string }
func (u *User) GetName() string { return u.Name // Panic if u is nil }
var user *User // nil name := user.GetName() // PANIC!
// CORRECT: Nil check in method func (u *User) GetName() string { if u == nil { return "" } return u.Name }
// CORRECT: Ensure non-nil before use user := &User{Name: "John"} name := user.GetName() // Safe ```
Nil interface:
```go // WRONG: Nil interface method call type Reader interface { Read([]byte) (int, error) }
var r Reader // nil interface var buf [100]byte n, err := r.Read(buf[:]) // PANIC!
// CORRECT: Check interface is not nil if r != nil { n, err = r.Read(buf[:]) } else { return 0, errors.New("reader is nil") }
// CORRECT: Use optional interface func process(r Reader) error { if r == nil { return errors.New("reader required") } // Use r safely } ```
Nil map access (doesn't panic for reads):
```go // Map read with nil map returns zero value (no panic) var m map[string]int value := m["key"] // Returns 0, no panic
// Map write to nil map DOES panic m["key"] = 42 // PANIC: assignment to entry in nil map
// CORRECT: Initialize map before use m := make(map[string]int) m["key"] = 42 // Safe
// CORRECT: Check and initialize if m == nil { m = make(map[string]int) } m["key"] = 42 ```
### 3. Fix index out of range
Slice bounds check:
```go // WRONG: Access without bounds check items := []string{"a", "b", "c"} item := items[5] // PANIC: index out of range
// CORRECT: Check bounds first if idx >= 0 && idx < len(items) { item = items[idx] } else { // Handle error }
// CORRECT: Safe accessor function func safeGet(items []string, idx int) (string, bool) { if idx >= 0 && idx < len(items) { return items[idx], true } return "", false }
item, ok := safeGet(items, 5) if !ok { // Handle missing item } ```
Range loop with index modification:
```go // WRONG: Modifying slice during iteration items := []int{1, 2, 3, 4, 5} for i := 0; i < len(items); i++ { if items[i] == 3 { items = append(items[:i], items[i+1:]...) // May cause index issues i-- // Adjust index } }
// CORRECT: Collect then modify var toRemove []int for i, item := range items { if item == 3 { toRemove = append(toRemove, i) } } // Remove collected indices
// CORRECT: Use filter pattern filtered := items[:0] for _, item := range items { if item != 3 { filtered = append(filtered, item) } } items = filtered ```
### 4. Fix type assertion panics
Type assertion with check:
```go // WRONG: Bare type assertion can panic var value interface{} = 42 str := value.(string) // PANIC: interface is int, not string
// CORRECT: Check with comma-ok idiom str, ok := value.(string) if !ok { // Handle type mismatch log.Printf("expected string, got %T", value) return }
// CORRECT: Type switch for multiple types switch v := value.(type) { case string: processString(v) case int: processInt(v) case bool: processBool(v) default: log.Printf("unknown type: %T", v) } ```
JSON unmarshaling safety:
```go // WRONG: Assuming JSON structure var data map[string]interface{} json.Unmarshal(raw, &data) name := data["user"].(map[string]interface{})["name"].(string) // May panic!
// CORRECT: Safe JSON parsing func getString(m map[string]interface{}, key string) string { v, ok := m[key] if !ok { return "" } s, ok := v.(string) if !ok { return "" } return s }
var data map[string]interface{} if err := json.Unmarshal(raw, &data); err != nil { return err } name := getString(data, "user") ```
### 5. Use defer/recover for graceful handling
Basic recover pattern:
```go func safeOperation() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) // Log the panic with stack trace log.Printf("panic recovered: %v\n%s", r, debug.Stack()) } }()
// Potentially panicking code riskyOperation() return nil }
// Usage if err := safeOperation(); err != nil { // Handle error gracefully log.Error(err) } ```
Recover in goroutine:
```go // ALWAYS recover in goroutines go func() { defer func() { if r := recover(); r != nil { log.Printf("goroutine panic: %v\n%s", r, debug.Stack()) // Optionally notify monitoring system sendAlert(r) } }()
// Goroutine work processRequest(req) }() ```
HTTP handler recovery:
```go func withRecovery(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { log.Printf("handler panic: %v\n%s", rec, debug.Stack()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next(w, r) } }
// Usage http.HandleFunc("/api", withRecovery(func(w http.ResponseWriter, r *http.Request) { // Handler code that might panic processData(r) })) ```
### 6. Fix channel-related panics
Send on closed channel:
```go // WRONG: Sending on closed channel ch := make(chan int) close(ch) ch <- 42 // PANIC: send on closed channel
// CORRECT: Track channel state type SafeChannel struct { ch chan int closed atomic.Bool }
func (s *SafeChannel) Send(val int) bool { if s.closed.Load() { return false } s.ch <- val return true }
func (s *SafeChannel) Close() { s.closed.Store(true) close(s.ch) } ```
Close of closed channel:
```go // WRONG: Closing channel multiple times ch := make(chan int) close(ch) close(ch) // PANIC: close of closed channel
// CORRECT: Use sync.Once var once sync.Once ch := make(chan int)
once.Do(func() { close(ch) }) once.Do(func() { close(ch) // Safe: only closes once })
// CORRECT: Single closer pattern func producer(ch chan<- int) { defer close(ch) // Only producer closes ch <- 1 ch <- 2 }
func consumer(ch <-chan int) { for val := range ch { // Receiver never closes process(val) } } ```
Nil channel operations:
```go // Reading from nil channel blocks forever var ch chan int // nil val := <-ch // Blocks forever (deadlock)
// Writing to nil channel blocks forever var ch chan int ch <- 42 // Blocks forever
// CORRECT: Initialize before use ch := make(chan int)
// CORRECT: Check in select var ch chan int // May be nil select { case val := <-ch: // This case never selected if ch is nil process(val) case <-time.After(100 * time.Millisecond): // Timeout instead of blocking forever } ```
### 7. Debug panics in production
Enable panic reporting:
```go // Add panic reporting in main() func main() { defer func() { if r := recover(); r != nil { // Send to error tracking (Sentry, Bugsnag, etc.) reportPanic(r, debug.Stack()) os.Exit(1) } }()
runApp() }
// Report panic to monitoring func reportPanic(r interface{}, stack []byte) { // Send to Sentry sentry.RecoverWithContext( context.Background(), r, &sentry.EventHint{Data: string(stack)}, ) sentry.Flush(5 * time.Second)
// Log locally log.Printf("PANIC: %v\n%s", r, stack) } ```
Use pprof for crash analysis:
```bash # Enable pprof HTTP endpoint import _ "net/http/pprof"
go func() { http.ListenAndServe("localhost:6060", nil) }()
# After crash, analyze profiles # Get goroutine profile at time of panic curl http://localhost:6060/debug/pprof/goroutine?debug=2 > goroutine.prof
# Analyze go tool pprof goroutine.prof (pprof) web # Visualize
# Get heap profile go tool pprof http://localhost:6060/debug/pprof/heap ```
Generate crash reports:
```bash # Set panic trace options export GOTRACEBACK=crash # Write core dump export GOTRACEBACK=system # Include runtime frames
# On Linux, get core dump ulimit -c unlimited ./app # Will create core file on panic
# Analyze core dump go tool pprof ./app core
# On macOS, uselldb lldb ./app (lldb) process launch # After crash: (lldb) thread backtrace all ```
Prevention
- Always check error returns, don't ignore them
- Use comma-ok idiom for type assertions
- Initialize maps, slices before use
- Add nil checks for pointer receivers
- Use defer/recover in goroutines
- Enable race detector: go test -race
- Use static analysis: go vet, staticcheck
- Write unit tests for edge cases
- Add integration tests with chaos testing
- Monitor production panics with alerting
Related Errors
- **Go channel deadlock**: Goroutines blocked waiting
- **Go channel send on closed channel**: Sending after close
- **Go goroutine leak**: Goroutines not terminating
- **Go context deadline exceeded**: Operation timeout
- **Go race detector positive**: Data races detected