Go 的秘密生活:panic 与 recover
Source: Dev.to
第21章:紧急刹车
“我想我把它弄死了,”伊桑低声说。
他盯着自己的终端。已经平稳运行了数周的 Web 服务器不见了。没有日志,没有关闭信息——只有命令提示符的突然返回。
“你做了什么?”埃莉诺靠在他肩上问。
“我只是发送了一个空 JSON 正文的 POST 请求,”伊桑说。“我本以为会得到 400 Bad Request 错误。结果整个服务器都消失了。”
“把日志给我看看,”埃莉诺说。
伊桑向上滚动。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a2b40]
goroutine 1 [running]:
main.HandleRequest(...)
/server/main.go:42 +0x45
main.main()
/server/main.go:80 +0x120
“啊,”埃莉诺点头说。“你正尝试访问一个
nil指针上的字段。在 C++ 中这会是段错误(segmentation fault)。在 Go 中它是一次 panic。”
“但我到处都在检查错误!”伊桑抗议道。
“panic 不是错误,”埃莉诺纠正道。“错误就像是礼貌的敲门声:‘不好意思,我打不开这个文件。’ 而 panic 则像是火警警报:‘一切都在燃烧。’ 它会立即停止执行,运行你的 defer 函数,然后让程序崩溃。”
解除
“所以……游戏结束了吗?” Ethan问道。
“不一定,” Eleanor说。“当 panic 发生时,它会沿着调用栈逐层向上冒泡。就像电梯在井道里自由下坠。”
她画了一个示意图:
HandleRequest panics! → CRASH
ServeHTTP stops → CRASH
main stops → EXIT PROGRAM
“我们需要紧急刹车,” Eleanor说。“我们需要一种机制,在自由下坠触底并终止进程之前将其停止。”
Source:
包含冲击波(恢复)
“在第 20 章我们学习了
defer,”她继续说。 “那是 唯一 可以阻止 panic 的地方。”
“为什么只能在那里?”
“因为当函数发生 panic 时,它会跳过所有普通代码。它 只 会执行延迟函数。如果你想捕获 panic,必须在 defer 栈中等待。”
她打开一个新文件来编写 中间件:
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// The safety net caught something!
log.Printf("Panic recovered: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
“
recover()函数很神奇,” Eleanor 解释道。 “如果程序正在 panic,recover()会捕获 panic 值并停止崩溃的电梯。它把控制权交还给你。如果程序 没有 panic,它只会返回nil,什么也不做。”
“所以它把一次崩溃转换成受控的着陆?”
“没错。它限制了冲击半径。一次错误请求会挂掉,但服务器仍然存活。”
Ethan 更新了他的 main 函数:
func main() {
handler := http.HandlerFunc(HandleRequest)
safeHandler := RecoveryMiddleware(handler) // Wrap it in the safety net
http.ListenAndServe(":8080", safeHandler)
}
他运行服务器并再次发送“毒药”请求。
- 终端:
Panic recovered: runtime error: invalid memory address or nil pointer dereference - 服务器状态: 仍在运行。
“它没有崩溃!” Ethan 欢呼道。
法治
“现在,”Eleanor说,声音变得严肃,“你已经知道如何使用
panic和recover。我的建议是:不要。”
Ethan眨了眨眼。“什么?”
“不要把
panic用于普通错误处理,”她警告道。“如果用户文件缺失,就返回一个error。如果数据库宕机,也返回一个error。”
“那我到底什么时候该用它?”
“只在不可能的情况下,”Eleanor说。“当程序员犯了如此严重的错误,以至于代码在逻辑上无法继续时才使用。或者在启动时,如果缺少必需的配置。”
func MustLoadConfig() *Config {
cfg, err := load("config.yaml")
if err != nil {
panic("cannot load config: " + err.Error())
// Acceptable: the app literally cannot start without this.
}
return cfg
}
“我们在系统的 边界——比如这个中间件中——使用
recover,以防单个有缺陷的请求把整艘船弄沉。但在你的业务逻辑内部呢?坚持使用错误。”
她补充了最后的警告:“记住:recover 只能在 同一个 goroutine 中起作用。如果你启动了一个后台工作者,而它 panic,这个中间件是救不了你的。整个程序会崩溃。”
Ethan看着自己的代码说:“所以……panic 用于崩溃,error 用于问题。”
“正是如此,”Eleanor微笑道。“而
recover就是降落伞,确保你还能活着去调试下一天。”
Source: …
第21章关键概念
Panic
- 一个内置函数,用于停止普通的控制流。
- 效果: 停止当前函数,执行所有
defer语句,然后使程序崩溃(除非被恢复)。 - 触发原因: 运行时错误(空指针解引用、索引越界)或显式调用
panic()。
Recover
- 一个内置函数,用来重新获得对正在 panic 的 goroutine 的控制权。
- 要求: 必须在 deferred 函数 中调用。
- 行为:
- 在 panic 期间调用时,捕获 panic 值并阻止崩溃。
- 正常调用时,返回
nil。
“Must” 模式
- Go 语言中惯用的做法:在 初始化 阶段如果必需资源加载失败就 panic(例如
regexp.MustCompile、加载配置)。 - 如果程序无法运行,应尽早崩溃。
Goroutine 边界
recover只能在 同一个 goroutine 中工作,针对发生 panic 的那个 goroutine。其他 goroutine 中的 panic 将导致程序终止,除非在该 goroutine 内部进行恢复。
关键要点
panic只会在 当前 goroutine 的调用栈上向上冒泡。- 如果你启动了一个普通的
go func(),并且它发生 panic,main中的recover无法 捕获它,程序将会崩溃。
下一章:Context 包。Ethan 学会如何取消耗时过长的请求。
Aaron Rose 是一名软件工程师兼技术作者,供职于 tech-reader.blog,也是《Think Like a Genius》的作者。Think Like a Genius.