Go 从 0 到精通 · 第 17 课:defer、panic、recover
Go 从 0 到精通 · 第 17 课:defer、panic、recover
学习定位:这是整套 Go 教程的第 17 课。
前置要求:已经完成第 16 课,掌握了error接口、错误创建、返回、包装和检查。
本课目标:理解defer的延迟执行机制,掌握panic和recover的异常流程控制,能正确使用defer管理资源,理解何时该用panic。
1. 本课你要解决的核心问题
第 16 课你学会了用 error 处理"可以预见的错误"——文件不存在、除数为零、网络超时等。
但还有两类问题 error 不太适合处理:
- 资源释放:打开文件后必须关闭,不管后续有没有出错
- 不可恢复的错误:数组越界、空指针解引用——程序不应该继续运行
Go 用三个工具解决这些问题:
defer:延迟执行,确保资源释放panic:触发恐慌,中断正常流程recover:捕获恐慌,防止程序崩溃
你需要搞明白以下问题:
defer的执行时机和顺序defer常见的坑(闭包陷阱)panic什么时候触发,怎么手动触发recover怎么捕获panic- 什么时候用
error,什么时候用panic
2. defer:延迟执行
2.1 基本用法
defer 注册一个函数调用,在当前函数返回之前执行:
1 | package main |
输出:
1 | 这句话先打印 |
2.2 最常见的用途:资源释放
1 | func readFile(path string) error { |
defer f.Close() 写在 os.Open 成功之后。这样不管后续代码怎么退出(正常 return、错误 return),文件都会被关闭。
2.3 多个 defer 的执行顺序
多个 defer 按后进先出(LIFO)顺序执行:
1 | func main() { |
输出:
1 | 函数体 |
像栈一样:最后注册的先执行。
2.4 defer 的参数在注册时就确定
1 | func main() { |
输出:
1 | 函数中的 x: 20 |
defer 注册时,参数就已经求值了。x 的值是 10,不是 20。
3. defer 的常见用法
3.1 文件操作
1 | func processFile(path string) error { |
3.2 互斥锁
1 | import "sync" |
3.3 记录函数执行时间
1 | import ( |
3.4 修改命名返回值
1 | func divide(a, b int) (result int, err error) { |
defer 的闭包可以访问和修改外层函数的命名返回值。这是 defer 最强大的特性之一。
4. defer 的常见坑
4.1 闭包变量陷阱
1 | func main() { |
输出:
1 | 3 |
三个 defer 都引用了同一个 i 变量,执行时 i 已经是 3。
修复:在循环中创建局部变量或传参:
1 | for i := 0; i < 3; i++ { |
4.2 defer 在错误路径中也执行
1 | func example() error { |
只有成功注册的 defer 才会执行。defer 语句本身可能因为前面的 return 而不被执行。
4.3 defer 和返回值的顺序
1 | func foo() int { |
return x 的过程是:先把 x 的值(0)复制到返回值,然后执行 defer,然后返回。defer 修改的是 x,不是返回值的副本。
如果用命名返回值:
1 | func foo() (x int) { |
命名返回值情况下,defer 修改的就是返回值本身。
5. panic:触发恐慌
5.1 什么是 panic
panic 是 Go 中的"恐慌"机制。一旦触发,当前函数立即停止执行,所有已注册的 defer 按 LIFO 顺序执行,然后程序沿着调用栈向上"冒泡",直到被 recover 捕获或程序崩溃。
5.2 自动触发 panic
Go 运行时在遇到不可恢复的错误时会自动触发 panic:
1 | func main() { |
5.3 手动触发 panic
1 | func validateAge(age int) { |
panic 接收任意类型的参数,通常是字符串。
5.4 panic 的执行流程
1 | func main() { |
输出:
1 | defer 2 |
- 触发
panic - 已注册的
defer继续执行(LIFO) panic之后的defer不会注册- 沿调用栈向上冒泡
6. recover:捕获恐慌
6.1 基本用法
recover 只能在 defer 中使用,用于捕获当前 goroutine 的 panic:
1 | func safeDivide(a, b int) (result int, err error) { |
6.2 recover 的规则
- 只在
defer函数中有效 - 只能捕获当前 goroutine 的
panic - 如果当前没有
panic,recover返回nil - 捕获后,
panic被终止,程序继续正常执行
6.3 只在最外层捕获
1 | func inner() { |
输出:
1 | outer 捕获: inner 出错 |
7. error vs panic
这是 Go 中一个非常重要的设计决策。
7.1 用 error 的场景
- 可预见的错误:文件不存在、网络超时、参数不合法
- 调用者应该处理的错误
- 错误是业务逻辑的一部分
1 | func openFile(path string) (*os.File, error) { |
7.2 用 panic 的场景
- 不可恢复的编程错误:数组越界、空指针
- 程序不应该继续运行的状态
- 初始化失败(配置严重错误)
1 | func init() { |
7.3 总结
能用
error处理的,不要用panic。
Go 的设计哲学是:错误是正常的,应该被显式处理。panic 是最后手段,表示"程序出了严重问题"。
8. 一段综合示例
1 | package main |
输出:
1 | 10 + 3 = 13 |
这个示例展示了:
defer+recover捕获运行时panic(如除零)error处理可预见的错误(负数检查)panic处理不应该发生的情况(未知操作符)
9. 常见坑总结
9.1 在非 defer 中调用 recover
1 | func main() { |
recover 只在 defer 函数中有意义。
9.2 用 panic 做流程控制
1 | // 绝对不要这样做 |
这是滥用 panic。用 error 处理可预见的错误。
9.3 捕获了 panic 但不做处理
1 | defer func() { |
至少应该记录日志。
9.4 defer 中的 panic
1 | func main() { |
defer 中的 panic 会替换掉原始的 panic。要小心。
9.5 忘记 defer 的执行顺序
1 | func cleanup() { |
后注册的 defer 先执行。如果资源之间有依赖关系,要注意顺序。
10. 本课练习
练习 1:defer 基础
要求:
- 写一个函数,用
defer模拟"进入函数"和"退出函数"的日志 - 观察
defer的执行顺序
练习 2:资源管理
要求:
- 写一个函数打开文件、读取内容、关闭文件
- 用
defer确保文件关闭 - 模拟读取失败的情况,验证
defer仍然执行
练习 3:recover 捕获
要求:
- 写一个
safeCall(fn func())函数,调用fn并捕获任何panic - 如果有
panic,打印错误信息但不让程序崩溃 - 测试传入一个会
panic的函数
练习 4:defer 闭包陷阱
要求:
- 用
for循环创建 5 个defer,每个defer打印循环变量 - 先写出"错误版本"(打印相同的值)
- 再写出"正确版本"(打印 0-4)
练习 5:error 与 panic 的选择
要求:
- 写一个栈(Stack)数据结构
Push和Pop操作- 栈空时
Pop:用error返回错误 - 栈满时
Push:用panic(因为这是编程错误——调用者应该控制好容量)
11. 自测题
11.1 概念题
defer的执行时机是什么?- 多个
defer的执行顺序是什么? defer注册时参数就确定了,还是执行时才确定?defer闭包中引用外部变量,拿到的是注册时的值还是执行时的值?panic触发后会发生什么?recover在什么情况下才有效?recover捕获panic后,程序会怎样?- 什么时候应该用
error?什么时候应该用panic? - 命名返回值和
defer配合使用有什么特别之处? defer f.Close()应该写在什么位置?
12. 本课总结
这一课你学到了 Go 中处理异常流程的三个工具。
你现在应该已经理解:
defer注册延迟执行的函数,在函数返回前执行(LIFO 顺序)defer最常用于资源释放(文件关闭、锁释放)defer的参数在注册时求值,闭包变量在执行时取值panic触发恐慌,中断正常流程,沿调用栈冒泡recover只在defer中有效,用于捕获panic- 能用
error处理的,不要用panic panic用于不可恢复的编程错误
13. 下一课预告
下一课我们学习阶段三的最后一课:包与模块管理。
会重点讲:
- 包(package)是什么,怎么组织
- 导出规则(首字母大写)
- 模块(module)和
go.mod - 依赖管理
学完下一课,你就能组织多文件、多包项目,阶段三就完成了。





