学习目标
- 真正理解 panic 在 Go 中的定位
- 明确 panic 与 error 的严格边界
- 掌握 recover 的正确使用位置
- 学会设计“不会把 panic 当业务逻辑”的工程代码
一、panic 在 Go 中到底是什么
在 Go 中:
panic 不是异常(exception),而是程序进入了“不可继续运行”的状态
panic 发生时:
- 当前函数立即停止执行
- 开始向上执行 defer
- 若无人 recover,程序直接崩溃并退出
Go 官方对 panic 的态度是:
panic 用于程序错误(bug),而不是业务错误
二、panic 的典型触发场景
以下情况非常适合 panic:
- nil 指针解引用
- 数组 / slice 越界
- 违反程序内部不变量
- 理论上“永远不该发生”的分支
示例:
func mustBePositive(n int) {
if n < 0 {
panic("n must be positive")
}
}说明:
- 这是程序员错误
- 不是用户输入错误
三、panic 不应该用于业务逻辑
错误示例(反模式)
func getUser(id int) *User {
if id == 0 {
panic("user not found")
}
return &User{ID: id}
}问题:
- 把“可预期的业务错误”当成致命错误
- 调用方无法正常处理
- 破坏程序稳定性
正确做法
func getUser(id int) (*User, error) {
if id == 0 {
return nil, ErrUserNotFound
}
return &User{ID: id}, nil
}工程结论:
能用 error 表达的,一定不要用 panic。
四、panic 与 error 的清晰分界
| 维度 | error | panic |
|---|---|---|
| 性质 | 可预期、可处理 | 不可预期、不可恢复 |
| 来源 | 业务、外部依赖 | 程序 bug |
| 处理方式 | 返回给调用方 | 立即中断执行 |
| 工程定位 | 正常控制流 | 程序边界 |
一句话记忆:
error 是“程序的一部分”,panic 是“程序出问题了”。
五、defer + panic 的执行顺序
示例:
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
panic("boom")
}输出结果:
defer 2
defer 1
panic: boom结论:
- panic 发生后,defer 仍然会执行
- 执行顺序仍然是 LIFO(后进先出)
六、recover:panic 的“捕获器”
recover 的作用:
在 panic 发生时,拦截 panic,防止程序崩溃。
重要规则:
- recover 只能在 defer 中生效
- recover 只能捕获“当前 goroutine”的 panic
示例:
func safeRun(fn func()) {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic recovered:", r)
}
}()
fn()
}七、recover 的正确使用位置(非常重要)
recover 应该放在哪里?
- HTTP Server 的最外层
- CLI 命令入口
- goroutine 的启动边界
- 框架 / 平台层
错误示例:在业务逻辑中 recover
func service() {
defer func() {
recover() // ❌ 吞掉 panic
}()
// 业务逻辑
}问题:
- 真正的 bug 被隐藏
- 程序处于未知状态继续运行
八、HTTP 服务中的 panic 兜底示例
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "internal server error", 500)
}
}()
next.ServeHTTP(w, r)
})
}说明:
- recover 只在最外层
- 业务层无需关心 panic
- 服务不会因为单次请求崩溃
九、goroutine 中的 panic 特别注意
示例:
go func() {
panic("boom")
}()结果:
- 整个进程直接崩溃
正确做法:
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("goroutine panic:", r)
}
}()
// goroutine 逻辑
}()预告:Day 11|goroutine 与并发模型基础
下一天你将系统理解:
- goroutine 的调度模型
- 并发 vs 并行的本质区别
- goroutine 的生命周期
- 为什么 goroutine “很轻,但不是免费”