Golang 常见 Panic 详解 Go 语言中的 panic 是一种异常机制,用于处理程序无法继续执行的错误情况。本文详细介绍常见的 panic 场景、原因和解决方案。
Panic 分类 flowchart LR
Root["Panic 分类"]
Root --> Fatal["致命异常 不能 recover"]
Root --> NonFatal["非致命异常 可以 recover"]
Fatal --> F1["数组越界"]
Fatal --> F2["map 并发读写"]
Fatal --> F3["栈溢出"]
Fatal --> F4["内存不足"]
NonFatal --> N1["空指针解引用"]
NonFatal --> N2["类型断言失败"]
NonFatal --> N3["写关闭的 channel"]
NonFatal --> N4["除零错误"]
NonFatal --> N5["手动 panic"]
style Root fill:#e1f5ff
style Fatal fill:#ffebee
style NonFatal fill:#e8f5e9
Panic 处理流程 sequenceDiagram
participant Code as 代码执行
participant Runtime as Go Runtime
participant Recover as recover()
participant Program as 程序
Code->>Runtime: 发生 panic
Runtime->>Runtime: 检查是否有 defer recover
alt 有 recover
Runtime->>Recover: 捕获 panic
Recover->>Code: 返回 panic 值
Code->>Program: 继续执行
else 没有 recover
Runtime->>Program: 程序崩溃
Program->>Program: 输出堆栈信息
end
致命异常 致命异常指的是 Go 运行时(runtime)直接 throw 的异常,这种异常不能使用 recover 捕获 ,程序会直接崩溃。
数组越界 Panic 原因 访问数组或切片时,索引超出有效范围。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func main () { arr := [3 ]int {1 , 2 , 3 } fmt.Println(arr[3 ]) slice := []int {1 , 2 , 3 } fmt.Println(slice[5 ]) str := "hello" fmt.Println(str[10 ]) }
错误信息 1 2 3 4 5 panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /path/to/file.go:8 +0x1a
如何避免 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func safeAccess (arr []int , index int ) (int , error ) { if index < 0 || index >= len (arr) { return 0 , fmt.Errorf("index %d out of range [0:%d]" , index, len (arr)) } return arr[index], nil } func main () { arr := []int {1 , 2 , 3 } if val, err := safeAccess(arr, 3 ); err != nil { fmt.Println("错误:" , err) } else { fmt.Println("值:" , val) } }
Map 并发读写 Panic 原因 多个 goroutine 同时对 map 进行读写操作,Go 的 map 不是并发安全的。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport ( "sync" "time" ) func main () { m := make (map [int ]int ) go func () { for i := 0 ; i < 1000 ; i++ { m[i] = i } }() go func () { for i := 0 ; i < 1000 ; i++ { m[i] = i * 2 } }() go func () { for i := 0 ; i < 1000 ; i++ { _ = m[i] } }() time.Sleep(1 * time.Second) }
错误信息 1 2 3 4 5 6 7 fatal error: concurrent map writes goroutine 18 [running]: runtime.throw(0x10a8b5e, 0x15) /usr/local/go/src/runtime/panic.go:774 +0x72 fp=0xc00002e758 sp=0xc00002e728 pc=0x1028d92 runtime.mapassign_fast64(0x109a1e0, 0xc000088180, 0x0, 0x0) /usr/local/go/src/runtime/map_fast64.go:101 +0x1f5 fp=0xc00002e798 sp=0xc00002e758 pc=0x100f5b5
解决方案 方案 1: 使用 sync.Mutex 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "sync" "time" ) type SafeMap struct { mu sync.RWMutex m map [int ]int } func NewSafeMap () *SafeMap { return &SafeMap{ m: make (map [int ]int ), } } func (sm *SafeMap) Set(key, value int ) { sm.mu.Lock() defer sm.mu.Unlock() sm.m[key] = value } func (sm *SafeMap) Get(key int ) (int , bool ) { sm.mu.RLock() defer sm.mu.RUnlock() val, ok := sm.m[key] return val, ok } func main () { sm := NewSafeMap() var wg sync.WaitGroup for i := 0 ; i < 10 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() for j := 0 ; j < 100 ; j++ { sm.Set(id*100 +j, j) } }(i) } wg.Wait() }
方案 2: 使用 sync.Map 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "sync" "time" ) func main () { var m sync.Map go func () { for i := 0 ; i < 1000 ; i++ { m.Store(i, i) } }() go func () { for i := 0 ; i < 1000 ; i++ { m.Store(i, i*2 ) } }() go func () { for i := 0 ; i < 1000 ; i++ { m.Load(i) } }() time.Sleep(1 * time.Second) }
检测并发读写 使用 go run -race 或 go test -race 可以检测并发问题:
非致命异常 非致命异常指的是 Go 运行时或者代码层级调用 panic 函数产生的异常,这种异常能够使用 recover 捕获 。
空指针解引用 Panic 原因 对 nil 指针进行解引用操作(访问字段或调用方法)。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" type Person struct { Name string Age int } func main () { var p *Person fmt.Println(p.Name) p.SayHello() } func (p *Person) SayHello() { fmt.Printf("Hello, I'm %s\n" , p.Name) }
错误信息 1 2 3 4 5 6 panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a8b5e] goroutine 1 [running]: main.main() /path/to/file.go:12 +0x1a
解决方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func safeAccess (p *Person) { if p == nil { fmt.Println("Person 指针为 nil" ) return } fmt.Println("Name:" , p.Name) } func main () { var p *Person safeAccess(p) defer func () { if r := recover (); r != nil { fmt.Println("捕获到 panic:" , r) } }() fmt.Println(p.Name) }
类型断言失败 Panic 原因 使用类型断言时,如果断言失败且没有使用 ok 接收返回值,会触发 panic。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport "fmt" func main () { var i interface {} = "hello" s := i.(int ) if s, ok := i.(int ); ok { fmt.Println("是 int 类型:" , s) } else { fmt.Println("不是 int 类型" ) } switch v := i.(type ) { case int : fmt.Println("是 int:" , v) case string : fmt.Println("是 string:" , v) default : fmt.Println("未知类型" ) } }
错误信息 1 2 3 4 5 panic: interface conversion: interface {} is string, not int goroutine 1 [running]: main.main() /path/to/file.go:8 +0x1a
安全使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func safeTypeAssert (i interface {}) { if s, ok := i.(string ); ok { fmt.Println("是 string:" , s) } else { fmt.Println("不是 string 类型" ) } switch v := i.(type ) { case string : fmt.Println("string:" , v) case int : fmt.Println("int:" , v) default : fmt.Println("其他类型" ) } }
写关闭的 Channel Panic 原因 向已关闭的 channel 写入数据会触发 panic。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "time" func main () { ch := make (chan int , 1 ) close (ch) ch <- 1 }
错误信息 1 2 3 4 5 panic: send on closed channel goroutine 1 [running]: main.main() /path/to/file.go:10 +0x1a
解决方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "sync" ) func safeChannelWrite (ch chan int , value int ) error { defer func () { if r := recover (); r != nil { fmt.Println("捕获到 panic:" , r) } }() select { case ch <- value: return nil default : return fmt.Errorf("channel 已满或已关闭" ) } } func main () { ch := make (chan int , 1 ) close (ch) if err := safeChannelWrite(ch, 1 ); err != nil { fmt.Println("错误:" , err) } }
最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "context" "fmt" "time" ) func producer (ctx context.Context, ch chan <- int ) { defer close (ch) for i := 0 ; i < 10 ; i++ { select { case <-ctx.Done(): return case ch <- i: fmt.Printf("发送: %d\n" , i) } } } func consumer (ch <-chan int ) { for val := range ch { fmt.Printf("接收: %d\n" , val) } } func main () { ch := make (chan int , 5 ) ctx, cancel := context.WithTimeout(context.Background(), 2 *time.Second) defer cancel() go producer(ctx, ch) consumer(ch) }
除零错误 Panic 原因 整数除以零会触发 panic。
示例代码 1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { a, b := 10 , 0 result := a / b fmt.Println(result) }
错误信息 1 2 3 4 5 panic: runtime error: integer divide by zero goroutine 1 [running]: main.main() /path/to/file.go:8 +0x1a
解决方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "errors" "fmt" ) func safeDivide (a, b int ) (int , error ) { if b == 0 { return 0 , errors.New("除数不能为零" ) } return a / b, nil } func main () { result, err := safeDivide(10 , 0 ) if err != nil { fmt.Println("错误:" , err) } else { fmt.Println("结果:" , result) } }
手动 Panic 使用场景 代码中主动调用 panic() 函数,通常用于处理不应该发生的错误情况。
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func processUser (userID int ) { if userID <= 0 { panic ("用户 ID 必须大于 0" ) } fmt.Printf("处理用户 %d\n" , userID) } func main () { defer func () { if r := recover (); r != nil { fmt.Printf("捕获到 panic: %v\n" , r) } }() processUser(0 ) fmt.Println("这行不会执行" ) }
最佳实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport ( "errors" "fmt" ) func processUserSafe (userID int ) error { if userID <= 0 { return errors.New("用户 ID 必须大于 0" ) } fmt.Printf("处理用户 %d\n" , userID) return nil } func mustProcessUser (userID int ) { if userID <= 0 { panic (fmt.Sprintf("无效的用户 ID: %d" , userID)) } } func main () { if err := processUserSafe(0 ); err != nil { fmt.Println("错误:" , err) } defer func () { if r := recover (); r != nil { fmt.Println("恢复:" , r) } }() mustProcessUser(0 ) }
Panic 和 Recover 机制 Recover 使用 flowchart TD
Start["函数执行"] --> Defer["defer recover()"]
Defer --> Code["执行代码"]
Code --> Panic{发生 panic?}
Panic -->|是| Recover["recover 捕获"]
Panic -->|否| Normal["正常返回"]
Recover --> Handle["处理 panic"]
Handle --> Return["返回错误或继续"]
style Start fill:#e1f5ff
style Recover fill:#fff3e0
style Normal fill:#e8f5e9
完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "runtime" ) func safeOperation () (err error ) { defer func () { if r := recover (); r != nil { err = fmt.Errorf("panic recovered: %v" , r) buf := make ([]byte , 4096 ) n := runtime.Stack(buf, false ) fmt.Printf("堆栈信息:\n%s\n" , buf[:n]) } }() arr := []int {1 , 2 , 3 } fmt.Println(arr[10 ]) return nil } func main () { if err := safeOperation(); err != nil { fmt.Println("操作失败:" , err) } else { fmt.Println("操作成功" ) } }
Recover 注意事项
必须在 defer 中调用 : recover 只能在 defer 函数中生效
只能捕获当前 goroutine : recover 不能捕获其他 goroutine 的 panic
调用栈限制 : recover 只能捕获当前函数及其调用栈中的 panic
错误示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func wrong () { if r := recover (); r != nil { fmt.Println(r) } panic ("test" ) } func correct () { defer func () { if r := recover (); r != nil { fmt.Println(r) } }() panic ("test" ) }
常见 Panic 场景总结 场景对比 flowchart LR
A["数组/切片越界"] -->|致命| B["程序崩溃"]
C["map 并发读写"] -->|致命| B
D["空指针解引用"] -->|可恢复| E["recover 捕获"]
F["类型断言失败"] -->|可恢复| E
G["写关闭 channel"] -->|可恢复| E
H["除零错误"] -->|可恢复| E
style B fill:#ffebee
style E fill:#e8f5e9
预防措施 1. 数组/切片访问 1 2 3 4 5 6 7 8 9 if index >= 0 && index < len (arr) { val = arr[index] } for i, v := range arr { }
2. Map 并发安全 1 2 3 4 5 6 7 8 9 var mu sync.RWMutexmu.Lock() m[key] = value mu.Unlock() var m sync.Mapm.Store(key, value)
3. 指针使用 1 2 3 4 5 6 7 8 9 10 11 if p != nil { p.DoSomething() } defer func () { if r := recover (); r != nil { } }()
4. 类型断言 1 2 3 4 5 6 7 8 9 10 if val, ok := i.(Type); ok { } switch v := i.(type ) {case Type: }
5. Channel 操作 1 2 3 4 5 6 7 8 9 10 11 select {case ch <- value: default : } ctx, cancel := context.WithCancel(context.Background()) defer cancel()
调试技巧 1. 获取堆栈信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "runtime" ) func printStack () { buf := make ([]byte , 4096 ) n := runtime.Stack(buf, true ) fmt.Printf("堆栈信息:\n%s\n" , buf[:n]) } func main () { defer func () { if r := recover (); r != nil { fmt.Println("Panic:" , r) printStack() } }() panic ("测试 panic" ) }
2. 使用 race detector 1 2 3 go run -race main.go go test -race ./...
3. 使用 pprof 分析 1 2 3 4 5 6 7 8 import _ "net/http/pprof" func main () { go func () { log.Println(http.ListenAndServe("localhost:6060" , nil )) }() }
总结 关键要点
致命异常 : 数组越界、map 并发读写等,无法 recover
非致命异常 : 空指针、类型断言等,可以 recover
预防为主 : 通过边界检查、nil 检查等方式预防 panic
合理使用 : panic 应该用于无法恢复的错误,一般情况返回 error
最佳实践
✅ 优先返回 error : 使用 error 处理可预期的错误
✅ 边界检查 : 访问数组、切片前检查索引
✅ nil 检查 : 使用指针前检查是否为 nil
✅ 并发安全 : 使用 sync.Mutex 或 sync.Map 保护共享资源
✅ defer recover : 在需要的地方使用 defer recover 捕获 panic
✅ 避免 panic : 尽量使用 error 而不是 panic
理解这些常见的 panic 场景,可以帮助我们编写更健壮的 Go 程序!