首页 / Go 语言入门教程 / panic 与 recover

Go 语言入门教程

panic 与 recover

本教程共 80 篇 · 第 26 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程panicrecover错误处理deferGo 异常

26. panic 与 recover

本节目标:理解 panic 的触发条件,掌握 recover 捕获 panic 的用法,学会判断什么时候该 panic 什么时候该返回 error。

panic 是什么

panic 是 Go 的内置函数,触发后程序会立即停止当前函数的执行,开始逐层执行 defer,最后程序退出。

func main() {
    fmt.Println("开始")
    panic("出大事了")
    fmt.Println("这行不会执行")
}

输出:

开始
panic: 出大事了

goroutine 1 [running]:
main.main()
        /path/to/main.go:4 +0x95
exit status 2

panic 后面的代码不会执行,程序打印错误信息和调用栈后退出。

什么时候会 panic

手动触发

panic("something went wrong")
panic(errors.New("数据库连接失败"))

运行时错误

很多运行时错误会自动触发 panic:

// 空指针解引用
var p *int
*p = 10  // panic: runtime error: invalid memory address

// 数组越界
nums := []int{1, 2, 3}
fmt.Println(nums[10])  // panic: runtime error: index out of range

// 除以零
var a, b int
c := a / b  // panic: runtime error: integer divide by zero

// 类型断言失败
var i interface{} = "hello"
n := i.(int)  // panic: interface conversion

// 关闭已关闭的 channel
ch := make(chan int)
close(ch)
close(ch)  // panic: close of closed channel

recover 是什么

recover 是 Go 的内置函数,用来捕获 panic,阻止程序崩溃。

recover 只能在 defer 函数中有效:

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("发生 panic:%v", r)
        }
    }()

    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", result)
}

输出:

错误:发生 panic:runtime error: integer divide by zero

a / bb 为 0 时会 panic,但 defer 里的 recover() 捕获了它,把它转换成了 error 返回。程序没有崩溃。

recover 的规则

必须在 defer 中调用

// 不在 defer 中,recover 返回 nil
func bad() {
    if r := recover(); r != nil {
        fmt.Println(r)  // 不会执行
    }
    panic("error")
}

recover 只在 defer 函数中才有效。直接调用 recover() 返回 nil

只捕获当前 goroutine 的 panic

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("捕获:", r)
        }
    }()

    go func() {
        panic("goroutine 里的 panic")
    }()

    time.Sleep(time.Second)
}

主 goroutine 的 defer 无法捕获子 goroutine 的 panic。程序会崩溃。

每个 goroutine 需要自己处理 panic:

go func() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("goroutine panic:", r)
        }
    }()

    panic("error")
}()
Warning

在 goroutine 中启动可能 panic 的代码,一定要加 defer + recover,否则整个程序会崩溃。

recover 后程序继续执行

recover 捕获 panic 后,函数正常返回,调用者继续执行:

func riskyFunc() {
    defer func() {
        recover()
    }()
    panic("oops")
}

func main() {
    riskyFunc()
    fmt.Println("继续执行")  // 会执行
}

panic 和 defer 的执行顺序

panic 触发后,会按 LIFO 顺序执行当前函数的所有 defer,然后逐层向上执行外层函数的 defer,直到被 recover 或程序退出:

func main() {
    defer fmt.Println("main defer 1")
    defer fmt.Println("main defer 2")

    inner()

    fmt.Println("这行不会执行")
}

func inner() {
    defer fmt.Println("inner defer 1")
    defer fmt.Println("inner defer 2")

    panic("出错了")
}

输出:

inner defer 2
inner defer 1
main defer 2
main defer 1
panic: 出错了
...

什么时候该 panic

Go 的错误处理哲学是:能用 error 处理的就用 error,不要用 panic

应该返回 error 的场景

  • 文件不存在
  • 网络超时
  • 用户输入无效
  • 数据库查询失败

这些都是预期的错误,调用者应该知道并处理。

可以 panic 的场景

  • 程序初始化失败:比如配置文件加载不了,程序无法启动
  • 不可能出现的状态:比如 switch 的 default 分支,理论上不应该到达
  • 编程错误:比如传了 nil 给不允许 nil 的参数
// 初始化失败,可以 panic
func initConfig(path string) {
    data, err := os.ReadFile(path)
    if err != nil {
        panic("无法读取配置文件:" + path)
    }
    // 解析配置...
}

// 不可能出现的状态
func colorToHex(c Color) string {
    switch c {
    case Red:
        return "#FF0000"
    case Green:
        return "#00FF00"
    case Blue:
        return "#0000FF"
    default:
        panic("未知的颜色:" + string(c))
    }
}

// 编程错误
func mustGetValue(m map[string]int, key string) int {
    v, ok := m[key]
    if !ok {
        panic("key 不存在:" + key)  // 调用者不应该传不存在的 key
    }
    return v
}

标准库的 panic 模式

标准库有一些 Must 开头的函数,它们在出错时 panic:

// regexp.MustCompile:正则表达式编译失败时 panic
re := regexp.MustCompile(`^\d+$`)

// template.Must:模板解析失败时 panic
tmpl := template.Must(template.New("name").Parse(`<h1>{{.}}</h1>`))

这些函数用于初始化阶段,如果你写的正则或模板是固定的,出错说明代码有 bug,应该 panic。

recover 的实际应用

HTTP 服务器

HTTP 服务器不能因为一个请求 panic 就整个崩溃:

func main() {
    http.HandleFunc("/", safeHandler)
    http.ListenAndServe(":8080", nil)
}

func safeHandler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            http.Error(w, "Internal Server Error", 500)
            log.Printf("panic: %v", err)
        }
    }()

    // 可能 panic 的处理逻辑
    handler(w, r)
}

安全执行

func SafeRun(f func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    f()
    return nil
}

func main() {
    err := SafeRun(func() {
        panic("oops")
    })
    fmt.Println(err)  // panic recovered: oops
}

goroutine 容错

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            defer func() {
                if r := recover(); r != nil {
                    log.Printf("goroutine %d panic: %v", id, r)
                }
            }()

            // 可能出错的逻辑
            if id == 1 {
                panic("bad goroutine")
            }
            fmt.Printf("goroutine %d done\n", id)
        }(i)
    }

    wg.Wait()
    fmt.Println("全部完成")
}

panic vs error 决策指南

场景处理方式
文件打不开返回 error
网络请求失败返回 error
用户输入无效返回 error
配置文件解析失败(启动阶段)panic
数组越界让它 panic(编程错误)
空指针解引用让它 panic(编程错误)
不可能的代码分支panic
第三方库可能 panicrecover
Tip

简单原则:如果是调用者应该处理的错误,返回 error。如果是程序员的 bug,panic。如果不确定,返回 error。

常见问题

recover 捕获不到 panic

// 错误:recover 不在 defer 中
func bad() {
    defer fmt.Println("cleanup")
    if r := recover(); r != nil {  // 这行在 panic 之前执行,返回 nil
        fmt.Println(r)
    }
    panic("error")
}

recover 必须在 defer 函数内部调用,不能直接在 defer 语句中。

recover 后返回零值

func risky() int {
    defer func() {
        recover()
    }()
    panic("error")
    // 不会执行到这里
    // recover 后函数返回零值 0
}

recover 捕获 panic 后,函数会返回对应类型的零值。如果你想返回特定值,用命名返回值:

func risky() (result int) {
    defer func() {
        if r := recover(); r != nil {
            result = -1
        }
    }()
    // ...
    return 42
}

不要用 panic 做 goto

// 不推荐:用 panic 控制流程
func process() {
    defer func() {
        recover()
    }()

    for _, item := range items {
        if shouldSkip(item) {
            panic("skip")  // 用 panic 跳出循环
        }
        // ...
    }
}

panic 不是流程控制工具。要用 breakreturn 或 error 来控制流程。

学完这节你能做什么

  • 理解 panic 的触发条件和执行流程
  • 用 defer + recover 捕获 panic
  • 判断什么时候该 panic 什么时候该返回 error
  • 在 goroutine 中正确处理 panic
  • 用 recover 保护 HTTP handler 等关键路径

下一节讲数组。