Appearance
11|panic 与 recover
panic 表示程序遇到了不可恢复的内部错误,当前 goroutine 的执行会立即停止,逐层弹出调用栈,执行沿途的 defer,最后整个 goroutine 崩溃。panic 不是处理预期错误的手段——网络超时、文件不存在、配置格式错误这些可预见的问题,都应该返回 error。panic 只适合表达"这种情况不应该发生,如果发生了说明程序有 bug"。recover 用于捕获 panic,让当前 goroutine 从崩溃状态中恢复,但它只能用在 defer 里,而且有严格的 goroutine 边界限制。
一、panic
调用 panic 后,当前 goroutine 里还没执行的代码都不会再执行。如果 panic 发生在主 goroutine(main 函数所在 goroutine)且未被恢复,整个进程退出:
go
package main
import "fmt"
func mustLoadConfig(path string) []byte {
data, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return data
}
func main() {
mustLoadConfig("/nonexistent.conf")
fmt.Println("done") // 不会执行
}panic 不是处理预期错误的手段。网络超时、文件不存在、配置格式错误这些可预见的问题,都应该返回 error。panic 只适合表达"这种情况不应该发生,如果发生了说明程序有 bug"。比如 nil 指针解引用、数组越界、类型断言失败,这些通常是代码缺陷,不是环境问题。
初始化阶段如果遇到致命错误(比如数据库连接串解析失败),panic 是合理的选择——不 panic 的话,程序带着错误配置继续启动,后续所有操作都会失败,而且错误原因被埋得更深。
二、recover
recover 用于捕获 panic,让当前 goroutine 从崩溃状态中恢复。它只能在 defer 里调用,在其他位置调用无效:
go
package main
import "fmt"
func safeCheck() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered from panic:", r)
}
}()
var p *int
fmt.Println(*p) // nil 指针解引用,触发 panic
}
func main() {
safeCheck()
fmt.Println("main continues")
}recover 返回的是 panic 时传入的值,类型是 interface{}。如果当前 goroutine 没有发生 panic,recover 返回 nil。
recover 只能捕获同一个 goroutine 里的 panic。子 goroutine 里发生的 panic 不会被父 goroutine 的 recover 捕获:
go
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
go func() {
panic("child goroutine panic") // 主 goroutine 的 recover 捕获不到
}()
time.Sleep(1 * time.Second)
fmt.Println("main continues") // 不会执行,因为子 goroutine panic 导致进程退出
}这是并发程序里最危险的认知误区之一:很多人认为在主函数里包一层 recover 就能保证程序不崩溃,实际上每个 goroutine 都需要自己的 recover 保护。
正确的做法是在每个 goroutine 内部包 recover:
go
func worker() {
defer func() {
if r := recover(); r != nil {
fmt.Println("worker recovered:", r)
}
}()
// 工作逻辑
}
func main() {
go worker()
}三、error 与 panic 的选择
| 场景 | 处理方式 | 原因 |
|---|---|---|
| 文件不存在、网络超时、配置格式错误 | 返回 error | 可预见、可恢复、调用方有选择权 |
| 程序 bug(nil 指针、数组越界) | panic | 不应发生,发生后继续执行结果不可信 |
| 初始化必须成功,失败则程序无法运行 | panic | 比如数据库连接串解析失败,不 panic 后续逻辑全错 |
| HTTP handler 内部 panic | recover + 返回 500 | 避免单个请求拖垮整个服务 |
HTTP 服务通常会在顶层 handler 里包 recover,把 panic 转成 500 响应,同时记录日志。这样单个请求的 bug 不会导致整个服务进程退出。
go
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v\n%s", rec, debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}四、常见错误
用 panic 处理预期错误
go
// 错误:网络超时是可预见的,不应 panic
if err != nil {
panic(err)
}网络波动、服务重启、DNS 延迟都是生产环境的常态。用 panic 处理这些,等于让程序在每次网络抖动时崩溃。
忽略 recover 的 goroutine 边界
在主 goroutine 里写 recover 不能保护子 goroutine。并发程序中,每个 goroutine 启动时都应该考虑是否需要自己的 defer recover。
滥用 recover
recover 不是万能保险。捕获 panic 后如果不记录日志、不上报指标、不采取修复措施,只是把崩溃隐藏起来,会让程序在错误状态下继续运行,产生更隐蔽的数据损坏。
recover 后不做任何处理
go
defer func() {
recover() // 静默吞掉 panic,不留痕迹
}()这种写法比不 recover 更危险——panic 发生了,但没有任何人知道。recover 后至少应该记录日志和堆栈信息。
在 defer 之外调用 recover
go
func main() {
if r := recover(); r != nil { // 无效,recover 返回 nil
fmt.Println("recovered")
}
}recover 只有在 defer 函数内部才有效。普通代码里调用 recover 永远返回 nil。