你好,我是利威尔xu。
这是「面试八股」专栏里 Go 错误处理方向的一篇。之前聊了并发十连问、内存管理十连问,这期聊 Go 里最容易被低估的三个语法特性------defer、panic、recover。它们看起来简单,但坑极多,面试官最爱拿它们设陷阱。
建议先收藏,文末那张"defer/panic/recover 坑位表",面试前扫一眼能救你一命。
文章目录
-
- 为什么这三个语法糖要单开一篇
- [第 1 问:defer 的参数是什么时候求值的?](#第 1 问:defer 的参数是什么时候求值的?)
- [第 2 问:多个 defer 的执行顺序是什么?](#第 2 问:多个 defer 的执行顺序是什么?)
- [第 3 问:defer 和 return 的先后顺序,返回值会被改吗?](#第 3 问:defer 和 return 的先后顺序,返回值会被改吗?)
- [第 4 问:panic 之后,defer 还会执行吗?](#第 4 问:panic 之后,defer 还会执行吗?)
- [第 5 问:recover 为什么有时候"救不回来"?](#第 5 问:recover 为什么有时候"救不回来"?)
- [第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?](#第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?)
- [defer/panic/recover 坑位表(建议收藏)](#defer/panic/recover 坑位表(建议收藏))
- 结尾
为什么这三个语法糖要单开一篇
defer、panic、recover 是 Go 错误处理的三件套。表面看:defer 延迟执行、panic 抛异常、recover 捕获异常,三句话讲完。但一旦问到"defer 的参数什么时候求值""defer 的执行顺序""recover 为什么有时救不回来",大多数人就露馅了。
这三件套的坑,是 Go 面试里"看起来都会、答起来全错"的重灾区。
第 1 问:defer 的参数是什么时候求值的?
答案框架 :defer 的参数在声明 defer 那一刻就求值了,而不是在执行 defer 时求值。
go
func f() {
i := 1
defer fmt.Println(i) // 参数 i 在这里就求值,是 1
i = 100 // 后面改 i,不影响 defer 里的 1
}
// 输出:1
但如果 defer 的是一个闭包,闭包里捕获的是变量引用,执行时才读最新值:
go
func f() {
i := 1
defer func() { fmt.Println(i) }() // 闭包捕获引用
i = 100
}
// 输出:100
追问应对:"为什么参数求值在声明时?"------因为 defer 本质是"把函数调用和它的实参先存起来,等函数返回时再执行"。存起来的时候,实参已经算好了。
第 2 问:多个 defer 的执行顺序是什么?
答案框架 :后进先出(LIFO),像叠盘子一样,最后 defer 的最先执行。
go
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 输出:3 2 1
追问应对:"这个顺序有什么用?"------典型用法是资源成对释放:先打开资源 A,再打开资源 B,defer 关闭时 B 先关、A 后关,顺序正好反着来,符合"先开的后关"的资源管理逻辑。
第 3 问:defer 和 return 的先后顺序,返回值会被改吗?
答案框架 :这是最经典的陷阱。defer 在 return 之后、函数真正返回之前 执行,所以命名返回值可以被 defer 修改。
go
func f() (result int) {
defer func() { result++ }() // 命名返回值,defer 能改
return 5
}
// 返回 6,不是 5
因为 return 5 其实是"先把 5 赋给命名返回值 result,再执行 defer,最后真正返回"。defer 里的 result++ 把 result 从 5 改成了 6。
追问应对:"如果返回值不是命名的呢?"------匿名返回值,defer 改不了。因为匿名返回值在 defer 执行时,值已经拷到返回位置了,defer 里改的是另一个副本。这就是"命名返回值才能被 defer 改"的原因。
第 4 问:panic 之后,defer 还会执行吗?
答案框架 :会 。panic 会沿着调用栈向上传播,每一层函数的 defer 都会被执行(这正是 defer 用来做清理的原因),直到被 recover 捕获或程序崩溃。
go
func f() {
defer fmt.Println("清理资源") // panic 后仍会执行
panic("出错了")
}
// 输出:清理资源,然后 panic
追问应对:"panic 后 defer 执行有什么用?"------保证资源释放、日志落盘。比如打开了文件/连接,panic 时 defer 里的 close 仍会执行,不会泄漏。这是 defer 的核心价值。
第 5 问:recover 为什么有时候"救不回来"?
答案框架 :recover 只有在 defer 函数里直接调用 才有效,且只能捕获当前 goroutine 的 panic。
无效的 recover:
go
func f() {
recover() // 直接调用,无效,panic 继续传播
panic("boom")
}
有效的 recover:
go
func f() {
defer func() {
if r := recover(); r != nil { // 必须在 defer 里
fmt.Println("捕获到:", r)
}
}()
panic("boom")
}
两个关键限制:
- recover 必须在 defer 函数里直接调用(不能嵌套在别的函数里)。
- recover 只能捕获本 goroutine 的 panic,别的 goroutine 的 panic 救不了。
追问应对 :"子 goroutine panic 了主 goroutine 能 recover 吗?"------不能。每个 goroutine 的 panic 只能自己 recover。所以生产上,子 goroutine 里的 panic 要么自己 recover,要么用 errgroup/统一 defer recover 兜底,否则整个进程崩溃。
第 6 问:panic 和 error 怎么选?什么该 panic 什么该返回 error?
答案框架:这是一道考工程素养的题。
用 error(绝大多数情况):可预期的、可恢复的错误,比如文件不存在、网络超时、参数非法。这些是"正常业务逻辑的一部分",用 error 返回让调用方处理。
用 panic(少数情况):不可恢复的、程序性错误,比如"slice 越界""空指针解引用""断言失败"。这些是"代码写错了",不该让调用方"优雅处理"。
一句话 :能预见的错误用 error,程序性的 bug 用 panic。 把 panic 当成正常的错误处理手段(比如用 panic/recover 做流程控制)是反模式。
追问应对 :"Go 标准库是怎么做的?"------标准库几乎都用 error,只有真正的程序错误(如 index out of range)才 panic。这体现了 Go 的哲学:错误是值,显式处理,而不是异常。
defer/panic/recover 坑位表(建议收藏)
| 考点 | 结论 | 常见错误答案 |
|---|---|---|
| defer 参数求值 | 声明时求值 | 说"执行时求值" |
| defer 闭包 | 捕获引用,执行时读最新值 | 混淆参数和闭包 |
| defer 顺序 | 后进先出 LIFO | 说"先进先出" |
| defer 改返回值 | 命名返回值可被改 | 说"defer 改不了返回值" |
| panic 后 defer | 会执行 | 说"panic 后 defer 不执行" |
| recover 位置 | 必须在 defer 里直接调 | 说"recover 随便调都行" |
| recover 范围 | 只能本 goroutine | 说"能跨 goroutine 捕获" |
| panic vs error | 可预期用 error,程序 bug 用 panic | 说"一律用 panic" |
结尾
defer、panic、recover 这三个语法糖,坑就藏在"求值时机""执行顺序""作用范围"这些细节里。吃透这张坑位表,面试里的相关陷阱题基本就踩不着了。
你被问过最刁钻的一道 defer 题是什么?是不是"defer 改返回值"或"recover 救不回来"这种?
下一篇面试八股,我接着聊 Go 的 interface 和反射,还是这个专栏。别走丢。
作者:利威尔xu | CSDN 专栏《面试八股》