--- tags: [go, golang, go基础语法, 异常捕获] create time: 2026-06-07 15:00 --- # Go 语言异常捕获 ## 概述 Go 用 `panic` 表示不可恢复的错误,用 `recover` + `defer` 捕获 panic。本文讲解 panic 的传递规则、recover 的正确用法,以及何时不该用 recover。 ## 正文 ### panic vs error:该用哪个? 这是 Go 开发者最常见的困惑之一: | | error | panic | |--|-------|-------| | 用途 | **预期内**的错误(文件不存在、参数无效) | **不应该发生**的严重错误(nil 指针解引用、数组越界) | | 处理方式 | if err != nil 检查 | defer + recover 捕获 | | 是否继续执行 | 是(处理后可继续) | 否(当前 goroutine 栈展开) | > [!tip] 💡 黄金法则 > **99% 的场景你应该用 error,而不是 panic。** panic 应该留给"程序状态已损坏、无法安全继续"的情况。 ### recover:唯一的安全网 ```go func main() { defer func() { if r := recover(); r != nil { fmt.Println("捕获到 panic:", r) } }() panic("出事了!") fmt.Println("这行不会执行") } // 输出: // 捕获到 panic: 出事了! ``` > [!note] 📝 recover 的关键限制 > - `recover()` **只能在 defer 中调用**才有效 > - 它恢复的是**当前 goroutine** 的 panic > - 被 recover 后,函数正常返回,调用方继续执行 ### panic 的传递规则 ```mermaid graph TD A["testPanic3 触发 panic"] --> B{"有 recover?"} B -->|否| C["栈展开,向上找"] C --> D["testPanic2 有 recover → 捕获"] D --> E["testPanic2 返回"] E --> F["testPanic1 继续执行"] F --> G["main 继续执行"] ``` 当 panic 没有被当前函数 recover 时,它会沿调用链向上传播,直到遇到 recover 或到达 `main`: ```go func testPanic3() { fmt.Println("上半部分") panic("boom!") fmt.Println("下半部分") // ❌ 不会执行 } func testPanic2() { defer func() { recover() }() // ✅ 捕获了来自 testPanic3 的 panic testPanic3() fmt.Println("testPanic2 继续") // 会执行 } func testPanic1() { testPanic2() fmt.Println("testPanic1 继续") // 会执行 } ``` 输出: ``` 上半部分 testPanic2 继续 testPanic1 继续 ``` > [!warning] ⚠️ 没有 recover 的后果 > 如果 panic 一路传播到 `main` 都没有被 recover,程序会崩溃并打印堆栈信息: > ``` > panic: boom! > goroutine 1 [running]: > ... > ``` ### 实战:安全的数学运算 ```go func SafeDivide(a, b int) (result int, errMsg string) { defer func() { if r := recover(); r != nil { errMsg = fmt.Sprintf("除零错误: %v", r) } }() return a / b, "" } val, msg := SafeDivide(10, 0) if msg != "" { log.Println(msg) // 除零错误: runtime error: integer divide by zero } ``` ### 常见使用场景 | 场景 | 说明 | |------|------| | 服务器中间件 | 捕获请求级 panic,避免整个服务崩溃 | | 并发池 | Worker 协程中 recover,防止单个 goroutine 挂掉影响其他任务 | | 插件系统 | 用户代码可能 panic,需要隔离保护 | ```go // 并发池中的典型模式 func Worker(id int, jobs <-chan int, results chan<- int) { defer func() { if r := recover(); r != nil { log.Printf("worker %d panicked: %v", id, r) } }() for j := range jobs { results <- process(j) } } ``` ### 关键要点总结 > [!warning] ⚠️ 不要滥用 recover > - 业务逻辑错误 → 用 error > - 编程错误(bug)→ panic 是可以的 > - 第三方库 panic → recover 保护你的程序 > - 每个函数都加 recover → 掩盖 bug,不利于调试 > [!info] ℹ️ 最佳实践 > 1. 在入口点(如 HTTP handler、main 函数顶层)加一层 recover > 2. 在并发 goroutine 中加 recover,防止单个协程 crash 扩散 > 3. 记录完整的 panic 堆栈(可用 `runtime.Stack()`)后再恢复