Go 的秘密生活:panic 与 recover

发布: (2026年2月10日 GMT+8 13:41)
8 分钟阅读
原文: Dev.to

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说,声音变得严肃,“你已经知道如何使用 panicrecover。我的建议是:不要。”

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.

0 浏览
Back to Blog

相关文章

阅读更多 »

Savior:低层设计

磨练 Go:Low‑Level Design 我回到绘图板上进行面试准备并提升我的问题解决能力。软件开发正处于一个...