Go里的panic、defer、recover到底怎么配合
Go 不用 try-catch,改用 panic、defer、recover 三件套。panic 抛异常,defer 保底执行,recover 捞回来继续跑。这套机制比传统异常处理更灵活,代码也更干净。
Go 里异常处理没有 try-catch,看起来像是少了东西,实际上用 panic、defer、recover 这三件套才是真正的大智若愚。
panic 抛异常的时候
panic 是 Go 里的炸弹,一旦丢出去,程序就开始崩塌。
通常我们不建议乱用 panic,应该用返回 error 这种温和的方式(接收方可以选择处理或忽略)。但有些场景确实无路可走,比如数组越界、nil 指针这种逻辑错误,就只能 panic。
// 主动 panic
func divide(a, b int) int {
if b == 0 {
panic("不能除以0啊老哥")
}
return a / b
}
// 被动 panic(比如 nil 指针)
var p *int
x := *p // 直接 panic: runtime error: invalid memory address
当 panic 发生时,程序不是立马就死,而是开始向上逐层返回——这个时候就轮到 defer 出手了。
defer 保底执行
defer 是 Go 的"临终遗言"机制:函数执行过程中遇到 panic,或者正常返回前,所有 defer 语句都会按后进先出(LIFO) 的顺序执行,就像一个压栈的过程。
func cleanup() {
defer func() { fmt.Println("清理第三个") }()
defer func() { fmt.Println("清理第二个") }()
defer func() { fmt.Println("清理第一个") }()
fmt.Println("做主要工作")
}
// 输出
// 做主要工作
// 清理第一个
// 清理第二个
// 清理第三个
所以 defer 的常见用途就是资源回收:关闭文件、释放锁、关闭数据库连接。
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 无论后面发生啥,这行保证会执行
return ioutil.ReadAll(f)
}
recover 捞回来
panic 一旦发生,defer 里的代码开始执行,这时候就有机会用 recover 捞一把。
recover 只在 defer 里有效,作用是拦截 panic 并返回它的值,让程序继续活下去;如果没有 panic,recover 返回 nil。
func safeRun() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("捞回来了:%v\n", err)
}
}()
panic("我要挂了")
fmt.Println("这行永远不会执行") // panic 后面的代码直接跳过
}
// 输出
// 捞回来了:我要挂了
注意:recover 后程序是活的,但不会回到 panic 那个位置继续执行,而是直接返回当前函数。
三件套完整配合
通常我们会这样用:先准备好兜底的 defer,里面放 recover 逻辑;执行核心代码,如果 panic 了就被 defer 捞回来。
func process(data []int) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("数据处理崩溃了:%v\n", err)
// 这里可以清理、记日志、或者继续往外抛
}
}()
// 核心代码
for i := 0; i < len(data)+5; i++ { // 故意越界
fmt.Printf("处理 %d\n", data[i])
}
}
func main() {
process([]int{1, 2, 3})
fmt.Println("程序继续")
}
// 输出
// 处理 1
// 处理 2
// 处理 3
// 数据处理崩溃了:runtime error: index out of range [3] with length 3
// 程序继续
进阶:在 recover 里重新抛
有时候我们想捞回来但不想完全吞下这个异常,可以在 recover 里再 panic 一次,让它继续往上传播。
func middlewareRecover() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("中间件捞到 panic:%v\n", err)
// 记个日志
panic(err) // 再抛一次,让上层继续处理
}
}()
panic("原始 panic")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("顶层捞到:%v\n", err)
}
}()
middlewareRecover()
}
// 输出
// 中间层捞到 panic:原始 panic
// 顶层捞到:原始 panic
这样我们就可以实现一个日志记录中间件的效果:在某一层拦截 panic、记好日志,再继续向上传播,让顶层有机会做统一的错误处理。
注意事项
recover 只能捞同一个 goroutine 里的 panic。如果在另一个 goroutine 里 panic,当前 goroutine 的 recover 是捞不到的。
go func() {
panic("我在 goroutine 里 panic 了") // 这会导致整个程序崩溃
}()
defer func() {
recover() // 捞不到上面那个 panic
}()
time.Sleep(time.Second)
所以起 goroutine 的时候,如果有风险,最好在 goroutine 内部就做好 recover。
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("goroutine 捞到:%v\n", err)
}
}()
panic("这次能捞住")
}()
time.Sleep(time.Second)
小结
panic、defer、recover 的组合其实很简洁:panic 抛问题、defer 保底执行、recover 捞异常。这套逻辑比 try-catch-finally 多了一份灵活性——defer 不是只为异常存在,平时用来做资源回收才是主要用途,panic 和 recover 反而是辅助角色。
理解了这三件套的配合,Go 的异常处理就没啥神秘的了,关键是记住 recover 只在 defer 里有效、只能捞同 goroutine 的 panic 这两点,大多数坑就避开了。
