Files
cs-note/hzh/GolangStar/Go语言基础/Go语言异常捕获.md
T

149 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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()`)后再恢复