Day 10|panic / recover 与程序边界控制 [Go 语言 30 天系统学习计划]

学习目标

  1. 真正理解 panic 在 Go 中的定位
  2. 明确 panic 与 error 的严格边界
  3. 掌握 recover 的正确使用位置
  4. 学会设计“不会把 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 的清晰分界

维度errorpanic
性质可预期、可处理不可预期、不可恢复
来源业务、外部依赖程序 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 “很轻,但不是免费”

发表评论