Kodewick
开始学 →
第 15 课,共 24 课目录

defer、panic 和 recover

第 15 课,共 24 课 · 第 4 部分 · 出错和函数 · 约 15 分钟

defer 把一个函数调用先放到一边,等外面这个函数结束的时候再运行。点单步看:带 defer 的那一行先被跳过,等 main 的最后一行走完才运行:

prog.go
1package main2 3import "fmt"4 5func main() {6	fmt.Println("开门")7	defer fmt.Println("关门")8	fmt.Println("接待客人")9}
输出
开门
接待客人
关门

「关门」写在第二行,却最后显示。收尾的代码可以紧挨着开头的代码写,到最后照样会做。

后进先出

一个函数里有好几个 defer,运行的顺序反过来:最后 defer 的最先运行,像一摞盘子。点单步看三个 defer 都等到循环结束:

prog.go
1package main2 3import "fmt"4 5func main() {6	for i := range 3 {7		defer fmt.Println("defer 的", i)8	}9	fmt.Println("循环完了")10}
输出
循环完了
defer 的 2
defer 的 1
defer 的 0

收尾的时候反着来正合适:最后打开的东西最先关上。

参数当场就算好

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5func main() {6	count := 17	defer fmt.Println("defer 时的 count", count)8	count = 109	fmt.Println("现在的 count", count)10}

想要最后那一刻的值,就 defer 一个没有名字的函数,让它运行时再去读变量。func() { … }() 是当场写一个函数并马上调用它(第 16 课细讲):

prog.go
1package main2 3import "fmt"4 5func main() {6	count := 17	defer func() {8		fmt.Println("最后的 count", count)9	}()10	count = 1011	fmt.Println("现在的 count", count)12}
输出
现在的 count 10
最后的 count 10

每条 return 都收尾

一个函数有好几个 return,收尾的那行就得在每个 return 前面都写一遍。用 defer 只写一次,函数不管从哪里结束都会运行:

prog.go
1package main2 3import "fmt"4 5func withdraw(balance, amount int) int {6	fmt.Println("打开账户")7	defer fmt.Println("关上账户")8	if amount > balance {9		fmt.Println("余额不够")10		return balance11	}12	return balance - amount13}14 15func main() {16	fmt.Println(withdraw(100, 30))17	fmt.Println(withdraw(100, 300))18}
输出
打开账户
关上账户
70
打开账户
余额不够
关上账户
100

注意「关上账户」出现在 70 前面:withdraw 要整个结束(包括 defer 的调用),main 才拿到结果去显示。真正的程序里,defer 用来关文件、解锁(第 22 课)。

panic

有些问题程序没法接着往下走:下标超出切片的末尾、往 nil map 里写。这时 Go 会 panic:停下这个函数,一路出去时运行 defer 的调用,显示一段话,结束程序。你也可以自己 panic:

prog.go
1package main2 3import "fmt"4 5func setPrice(price int) int {6	if price < 0 {7		panic("价格不能是负数")8	}9	return price10}11 12func main() {13	defer fmt.Println("main 里 defer 的还是会运行")14	fmt.Println(setPrice(30))15	fmt.Println(setPrice(-5))16	fmt.Println("这行不会显示")17}
输出
30
main 里 defer 的还是会运行
panic: 价格不能是负数

goroutine 1 [running]:
main.setPrice(...)
	/tmp/sandbox/prog.go:7 +0x1d
main.main(...)
	/tmp/sandbox/prog.go:15 +0x1d
exit status 2

发生了什么: 程序 panic 了:上面是 Go 的原文,下面一行是出事的地方。

先显示 30。接着 setPrice(-5) panic 了:main 里 defer 的那行照样运行,然后 Go 显示 panic: 价格不能是负数、调用栈(main.setPrice 被 main.main 调用,带着行号)和 exit status 2。「这行不会显示」真的没有显示。

recover

在 defer 的函数里调用 recover(),能让 panic 停下来。它返回交给 panic 的那个值(没有 panic 时返回 nil),然后函数正常返回。常见的用法是把 panic 变成一个错误:

prog.go
1package main2 3import "fmt"4 5func safeDivide(a, b int) (result int, err error) {6	defer func() {7		if r := recover(); r != nil {8			err = fmt.Errorf("接住了:%v", r)9		}10	}()11	return a / b, nil12}13 14func main() {15	fmt.Println(safeDivide(10, 2))16	fmt.Println(safeDivide(10, 0))17	fmt.Println("还在运行")18}
输出
5 <nil>
0 接住了:runtime error: integer divide by zero
还在运行

10 / 0 会 panic。defer 的函数接住它,给命名返回值 err 赋值(命名返回值,第 5 课);result 还是零值 0。没有 panic: 那段话,也没有 exit status 2:程序接着走。recover 只有直接写在 defer 的函数里才管用,在别的地方调用只会返回 nil。

什么时候用 panic

几乎不用。输入不对、文件不存在、钱不够:这些都是会发生的事,该由调用的地方决定怎么办,所以返回一个 error(第 14 课)。panic 留给「除非程序本身有 bug,否则不可能发生」的情况。就算这样,能返回错误还是返回错误。

情况用什么
用户输入了 abc,不是数字返回 error
map 里没有这个键v, ok := m[k] 或者 error
代码按理永远走不到的地方panic
一个 panic 不能让整个程序停下在 defer 的函数里 recover

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

用 defer 倒着数

现在 countdown(3) 先显示 准备,再显示 1、2、3。只改一行,让数字在 准备 后面倒着出来(3、2、1)。

要显示
准备
3
2
1
准备
2
1
exercise1.go
1package main2 3import "fmt"4 5func countdown(n int) {6	fmt.Println("准备")7	for i := 1; i <= n; i++ {8		fmt.Println(i)9	}10}11 12func main() {13	countdown(3)14	countdown(2)15}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:收银机忘了关

库存不够时,sell 提前返回,从来不显示 关上收银机。用 defer 让两条路都会关上收银机。

要显示
打开收银机
关上收银机
还剩 3
打开收银机
只剩 5 个
关上收银机
还剩 5
exercise2.go
1package main2 3import "fmt"4 5func sell(stock, qty int) int {6	fmt.Println("打开收银机")7	if qty > stock {8		fmt.Println("只剩", stock, "个")9		return stock10	}11	fmt.Println("关上收银机")12	return stock - qty13}14 15func main() {16	fmt.Println("还剩", sell(5, 2))17	fmt.Println("还剩", sell(5, 9))18}
输出
点「运行」或按 Ctrl+Enter
练习 3

把 panic 变成错误

safeGet(list, 5) 会 panic:下标越界。加一个带 recover 的 defer 函数,把 err 设成 fmt.Errorf("接住了:%v", r),让程序把两行都显示出来。

要显示
30 <nil>
0 接住了:runtime error: index out of range [5] with length 3
exercise3.go
1package main2 3import "fmt"4 5func safeGet(list []int, i int) (v int, err error) {6	return list[i], nil7}8 9func main() {10	prices := []int{10, 20, 30}11	fmt.Println(safeGet(prices, 2))12	fmt.Println(safeGet(prices, 5))13}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • defer f() 在外面的函数结束时运行 f(),不管函数怎么结束。
  • 好几个 defer 后进先出。
  • defer 的调用的参数在 defer 那一行就算好;defer func() { … }() 到最后才读变量。
  • panic 停下函数、运行 defer 的调用,然后用 panic: … 和 exit status 2 结束程序。
  • 在 defer 的函数里调用 recover() 能让 panic 停下;配合命名返回值,可以把它变成一个错误。
  • 会发生的事返回错误;panic 留给 bug。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。