Go 从 0 到精通 · 第 17 课:deferpanicrecover

学习定位:这是整套 Go 教程的第 17 课。
前置要求:已经完成第 16 课,掌握了 error 接口、错误创建、返回、包装和检查。
本课目标:理解 defer 的延迟执行机制,掌握 panicrecover 的异常流程控制,能正确使用 defer 管理资源,理解何时该用 panic


1. 本课你要解决的核心问题

第 16 课你学会了用 error 处理"可以预见的错误"——文件不存在、除数为零、网络超时等。

但还有两类问题 error 不太适合处理:

  1. 资源释放:打开文件后必须关闭,不管后续有没有出错
  2. 不可恢复的错误:数组越界、空指针解引用——程序不应该继续运行

Go 用三个工具解决这些问题:

  • defer:延迟执行,确保资源释放
  • panic:触发恐慌,中断正常流程
  • recover:捕获恐慌,防止程序崩溃

你需要搞明白以下问题:

  • defer 的执行时机和顺序
  • defer 常见的坑(闭包陷阱)
  • panic 什么时候触发,怎么手动触发
  • recover 怎么捕获 panic
  • 什么时候用 error,什么时候用 panic

2. defer:延迟执行

2.1 基本用法

defer 注册一个函数调用,在当前函数返回之前执行:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
defer fmt.Println("这句话最后打印")
fmt.Println("这句话先打印")
}

输出:

1
2
这句话先打印
这句话最后打印

2.2 最常见的用途:资源释放

1
2
3
4
5
6
7
8
9
10
11
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 确保文件一定会关闭

// 读取文件内容...
// 不管这里有没有 return,f.Close() 都会被调用
return nil
}

defer f.Close() 写在 os.Open 成功之后。这样不管后续代码怎么退出(正常 return、错误 return),文件都会被关闭。

2.3 多个 defer 的执行顺序

多个 defer后进先出(LIFO)顺序执行:

1
2
3
4
5
6
func main() {
defer fmt.Println("第一个 defer")
defer fmt.Println("第二个 defer")
defer fmt.Println("第三个 defer")
fmt.Println("函数体")
}

输出:

1
2
3
4
函数体
第三个 defer
第二个 defer
第一个 defer

像栈一样:最后注册的先执行。

2.4 defer 的参数在注册时就确定

1
2
3
4
5
6
func main() {
x := 10
defer fmt.Println("defer 中的 x:", x) // 此时 x = 10
x = 20
fmt.Println("函数中的 x:", x)
}

输出:

1
2
函数中的 x: 20
defer 中的 x: 10

defer 注册时,参数就已经求值了。x 的值是 10,不是 20


3. defer 的常见用法

3.1 文件操作

1
2
3
4
5
6
7
8
9
10
func processFile(path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()

_, err = f.WriteString("Hello, Go!")
return err
}

3.2 互斥锁

1
2
3
4
5
6
7
8
9
10
11
import "sync"

var mu sync.Mutex
var data map[string]int

func update(key string, value int) {
mu.Lock()
defer mu.Unlock() // 确保锁一定会释放

data[key] = value
}

3.3 记录函数执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"fmt"
"time"
)

func slowOperation() {
start := time.Now()
defer func() {
fmt.Println("耗时:", time.Since(start))
}()

// 模拟耗时操作
time.Sleep(2 * time.Second)
}

3.4 修改命名返回值

1
2
3
4
5
6
7
8
9
func divide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("内部错误: %v", r)
}
}()

return a / b, nil // b=0 时会 panic
}

defer 的闭包可以访问和修改外层函数的命名返回值。这是 defer 最强大的特性之一。


4. defer 的常见坑

4.1 闭包变量陷阱

1
2
3
4
5
6
7
func main() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i) // 闭包引用的是变量 i,不是值
}()
}
}

输出:

1
2
3
3
3
3

三个 defer 都引用了同一个 i 变量,执行时 i 已经是 3

修复:在循环中创建局部变量或传参:

1
2
3
4
5
6
7
8
9
10
11
12
for i := 0; i < 3; i++ {
i := i // 创建局部副本
defer func() {
fmt.Println(i)
}()
}
// 或者
for i := 0; i < 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i) // 参数在注册时求值
}

4.2 defer 在错误路径中也执行

1
2
3
4
5
6
7
8
9
10
func example() error {
f, err := os.Create("/tmp/test.txt")
if err != nil {
return err // defer 不会执行(因为 f 没创建成功)
}
defer f.Close() // 这个 defer 注册了

// 即使后面出错 return,f.Close() 也会执行
return nil
}

只有成功注册的 defer 才会执行。defer 语句本身可能因为前面的 return 而不被执行。

4.3 defer 和返回值的顺序

1
2
3
4
5
func foo() int {
x := 0
defer func() { x++ }()
return x // 返回 0,不是 1
}

return x 的过程是:先把 x 的值(0)复制到返回值,然后执行 defer,然后返回。defer 修改的是 x,不是返回值的副本。

如果用命名返回值:

1
2
3
4
5
func foo() (x int) {
x = 0
defer func() { x++ }()
return // 返回 1
}

命名返回值情况下,defer 修改的就是返回值本身。


5. panic:触发恐慌

5.1 什么是 panic

panic 是 Go 中的"恐慌"机制。一旦触发,当前函数立即停止执行,所有已注册的 defer 按 LIFO 顺序执行,然后程序沿着调用栈向上"冒泡",直到被 recover 捕获或程序崩溃。

5.2 自动触发 panic

Go 运行时在遇到不可恢复的错误时会自动触发 panic

1
2
3
4
5
6
7
8
9
func main() {
var s []int
fmt.Println(s[0]) // panic: runtime error: index out of range
}

func main() {
var p *int
fmt.Println(*p) // panic: runtime error: nil pointer dereference
}

5.3 手动触发 panic

1
2
3
4
5
6
7
8
9
10
func validateAge(age int) {
if age < 0 {
panic("年龄不能为负数")
}
fmt.Println("年龄:", age)
}

func main() {
validateAge(-1) // panic: 年龄不能为负数
}

panic 接收任意类型的参数,通常是字符串。

5.4 panic 的执行流程

1
2
3
4
5
6
7
8
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")

panic("出错了")

defer fmt.Println("defer 3") // 不会执行
}

输出:

1
2
3
4
5
6
7
defer 2
defer 1
panic: 出错了

goroutine 1 [running]:
main.main()
...
  1. 触发 panic
  2. 已注册的 defer 继续执行(LIFO)
  3. panic 之后的 defer 不会注册
  4. 沿调用栈向上冒泡

6. recover:捕获恐慌

6.1 基本用法

recover 只能在 defer 中使用,用于捕获当前 goroutine 的 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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) // 错误: 捕获到 panic: runtime error: integer divide by zero
} else {
fmt.Println("结果:", result)
}
}

6.2 recover 的规则

  • 只在 defer 函数中有效
  • 只能捕获当前 goroutinepanic
  • 如果当前没有 panicrecover 返回 nil
  • 捕获后,panic 被终止,程序继续正常执行

6.3 只在最外层捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func inner() {
panic("inner 出错")
}

func outer() {
defer func() {
if r := recover(); r != nil {
fmt.Println("outer 捕获:", r)
}
}()
inner() // panic 会冒泡到这里,被 recover 捕获
}

func main() {
outer()
fmt.Println("程序继续运行")
}

输出:

1
2
outer 捕获: inner 出错
程序继续运行

7. error vs panic

这是 Go 中一个非常重要的设计决策。

7.1 用 error 的场景

  • 可预见的错误:文件不存在、网络超时、参数不合法
  • 调用者应该处理的错误
  • 错误是业务逻辑的一部分
1
2
3
4
func openFile(path string) (*os.File, error) {
// 文件不存在是可预见的,返回 error
return os.Open(path)
}

7.2 用 panic 的场景

  • 不可恢复的编程错误:数组越界、空指针
  • 程序不应该继续运行的状态
  • 初始化失败(配置严重错误)
1
2
3
4
5
func init() {
if config == nil {
panic("配置未初始化,程序无法启动")
}
}

7.3 总结

能用 error 处理的,不要用 panic

Go 的设计哲学是:错误是正常的,应该被显式处理。panic 是最后手段,表示"程序出了严重问题"。


8. 一段综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"errors"
"fmt"
)

// 自定义错误
var ErrNegative = errors.New("数值不能为负数")

// 安全计算函数
func safeCalc(a, b int, op string) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("计算异常: %v", r)
}
}()

if a < 0 || b < 0 {
return 0, ErrNegative
}

switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
return a / b, nil // b=0 时 panic
default:
panic(fmt.Sprintf("未知操作: %s", op))
}
}

func main() {
tests := []struct {
a, b int
op string
}{
{10, 3, "+"},
{10, 3, "/"},
{10, 0, "/"}, // 除零
{-1, 3, "+"}, // 负数
{10, 3, "%"}, // 未知操作
}

for _, t := range tests {
result, err := safeCalc(t.a, t.b, t.op)
if err != nil {
fmt.Printf("%d %s %d → 错误: %v\n", t.a, t.op, t.b, err)
} else {
fmt.Printf("%d %s %d = %d\n", t.a, t.op, t.b, result)
}
}
}

输出:

1
2
3
4
5
10 + 3 = 13
10 / 3 = 3
10 / 0 → 错误: 计算异常: runtime error: integer divide by zero
-1 + 3 → 错误: 数值不能为负数
10 % 3 → 错误: 计算异常: 未知操作: %

这个示例展示了:

  • defer + recover 捕获运行时 panic(如除零)
  • error 处理可预见的错误(负数检查)
  • panic 处理不应该发生的情况(未知操作符)

9. 常见坑总结

9.1 在非 defer 中调用 recover

1
2
3
4
func main() {
panic("oops")
recover() // 不会执行,而且不在 defer 中也没有效果
}

recover 只在 defer 函数中有意义。

9.2 用 panic 做流程控制

1
2
3
4
5
6
7
8
9
10
11
12
// 绝对不要这样做
func findUser(id int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("用户不存在")
}
}()

if id <= 0 {
panic("not found")
}
}

这是滥用 panic。用 error 处理可预见的错误。

9.3 捕获了 panic 但不做处理

1
2
3
defer func() {
recover() // 捕获了但什么都没做,错误被吞掉了
}()

至少应该记录日志。

9.4 defer 中的 panic

1
2
3
4
5
6
func main() {
defer func() {
panic("defer 中的 panic") // 会覆盖之前的 panic
}()
panic("原始 panic")
}

defer 中的 panic 会替换掉原始的 panic。要小心。

9.5 忘记 defer 的执行顺序

1
2
3
4
5
func cleanup() {
defer db.Close() // 最后执行
defer log.Close() // 倒数第二
defer cache.Close() // 最先执行
}

后注册的 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:errorpanic 的选择

要求:

  • 写一个栈(Stack)数据结构
  • PushPop 操作
  • 栈空时 Pop:用 error 返回错误
  • 栈满时 Push:用 panic(因为这是编程错误——调用者应该控制好容量)

11. 自测题

11.1 概念题

  1. defer 的执行时机是什么?
  2. 多个 defer 的执行顺序是什么?
  3. defer 注册时参数就确定了,还是执行时才确定?
  4. defer 闭包中引用外部变量,拿到的是注册时的值还是执行时的值?
  5. panic 触发后会发生什么?
  6. recover 在什么情况下才有效?
  7. recover 捕获 panic 后,程序会怎样?
  8. 什么时候应该用 error?什么时候应该用 panic
  9. 命名返回值和 defer 配合使用有什么特别之处?
  10. defer f.Close() 应该写在什么位置?

12. 本课总结

这一课你学到了 Go 中处理异常流程的三个工具。

你现在应该已经理解:

  • defer 注册延迟执行的函数,在函数返回前执行(LIFO 顺序)
  • defer 最常用于资源释放(文件关闭、锁释放)
  • defer 的参数在注册时求值,闭包变量在执行时取值
  • panic 触发恐慌,中断正常流程,沿调用栈冒泡
  • recover 只在 defer 中有效,用于捕获 panic
  • 能用 error 处理的,不要用 panic
  • panic 用于不可恢复的编程错误

13. 下一课预告

下一课我们学习阶段三的最后一课:包与模块管理

会重点讲:

  • 包(package)是什么,怎么组织
  • 导出规则(首字母大写)
  • 模块(module)和 go.mod
  • 依赖管理

学完下一课,你就能组织多文件、多包项目,阶段三就完成了。