panic 与 recover
本教程共 80 篇 · 第 26 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
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 / b 在 b 为 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 |
| 第三方库可能 panic | recover |
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 不是流程控制工具。要用 break、return 或 error 来控制流程。
学完这节你能做什么
- 理解 panic 的触发条件和执行流程
- 用 defer + recover 捕获 panic
- 判断什么时候该 panic 什么时候该返回 error
- 在 goroutine 中正确处理 panic
- 用 recover 保护 HTTP handler 等关键路径
下一节讲数组。