defer、panic 和 recover
defer 把一个函数调用先放到一边,等外面这个函数结束的时候再运行。点单步看:带 defer 的那一行先被跳过,等 main 的最后一行走完才运行:
1package main2 3import "fmt"4 5func main() {6 fmt.Println("开门")7 defer fmt.Println("关门")8 fmt.Println("接待客人")9}
开门 接待客人 关门
「关门」写在第二行,却最后显示。收尾的代码可以紧挨着开头的代码写,到最后照样会做。
后进先出
一个函数里有好几个 defer,运行的顺序反过来:最后 defer 的最先运行,像一摞盘子。点单步看三个 defer 都等到循环结束:
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 那一行就算好了,那时 count 还是 1。
想要最后那一刻的值,就 defer 一个没有名字的函数,让它运行时再去读变量。func() { … }() 是当场写一个函数并马上调用它(第 16 课细讲):
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 只写一次,函数不管从哪里结束都会运行:
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:
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 变成一个错误:
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 |
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
用 defer 倒着数
✓现在 countdown(3) 先显示 准备,再显示 1、2、3。只改一行,让数字在 准备 后面倒着出来(3、2、1)。
准备 3 2 1 准备 2 1
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}
修好它:收银机忘了关
✓库存不够时,sell 提前返回,从来不显示 关上收银机。用 defer 让两条路都会关上收银机。
打开收银机 关上收银机 还剩 3 打开收银机 只剩 5 个 关上收银机 还剩 5
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}
把 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
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}
这一课学会了
defer f()在外面的函数结束时运行f(),不管函数怎么结束。- 好几个 defer 后进先出。
- defer 的调用的参数在
defer那一行就算好;defer func() { … }()到最后才读变量。 panic停下函数、运行 defer 的调用,然后用panic: …和exit status 2结束程序。- 在 defer 的函数里调用
recover()能让 panic 停下;配合命名返回值,可以把它变成一个错误。 - 会发生的事返回错误;
panic留给 bug。