小马别过河
2022/09/27阅读:22主题:默认主题
go 概念[译]: Defer, Panic, and Recover
Go语言有常用的流控制机制:if、for、switch、goto。同时go
关键字可以让代码跑在独立的协程上(goroutine)。本文介绍比较少见的几个:defer、panic 和 recover。
defer
defer
关键字把一个函数调用追加到一个列表。等defer
所在的函数 return 以后,这个列表内的函数,会依次被执行。defer
通常用于简化各种释放资源操作。
比如,下面的函数用于把一个文件的内容,复制到另外一个文件。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
这段代码能跑通,但是有一个bug。如果os.Create
调用失败,函数会在没有关闭 src 句柄的情况下,直接return。这个bug很容易修复,只要在第二个return前,加上 src.Close()
即可。但是如果逻辑更加复杂的话,就不那么容易被注意和解决了。使用defer
语句我们可以确保文件永远会被关闭。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
defer
语句允许我们打开完文件后关闭该文件,不用顾虑函数有多少个return
语句。
defer
语句很直接且可预测。这里有3个简单的规则。
1. defer函数的参数在defer出现时,就已经确定了。
比如下面例子,参数i
在Println
被加入 defer 列表的时候,就已经确定了。所以下面的例子会打印"0"。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
2. defer函数调用顺序是后进先出(LIFO)。
所以下面的输出是"3210":
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
3. defer函数可以对返回值进行读取和赋值。
下面的例子,会先确定i = 1
,然后执行 i++
,最后return i
。所以返回值是2。
func c() (i int) {
defer func() { i++ }()
return 1
}
Panic 和 Recover
Panic
是一个内置函数,用于中断控制流并开始"恐慌"(panicking)。当函数F
调用panic
时:
-
F
函数停止执行 -
defer 函数正常调用 -
F
函数返回给调用者
对于F
的调用者,相当于调用了panic
,直到所有进程的调用栈都返回了,此时程序会崩溃(crash)。Panics 可以通过调用 panic
函数直接触发,也可以由运行时引发,比如访问数组越界等。
Recover
是一个内置函数,用于重新取得一个已经 panic 的 goroutine 的控制权。Recover
只能用在 defer 函数里面,在其他地方调用 recover,只会返回 nil,不会有其他效果。如果当前 goroutine 正在 panic,recover 会捕获 panic 的值,并恢复运行。
这里有个例子,用于演示 panic 和 defer 的机制。
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
函数g
的参数i
如果大于3就会panic,否则就i+1
递归调用自己。函数f
在 defer 调用 recover 方法,并打印捕获到的 panic 的值。
程序输出如下:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
如果我们把函数f
的 recover 移除掉,panic就会返回到调用栈的最顶部,接着程序中断。输出如下:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
有关 panic 和 recover 的真实示例,可以参阅 Go 标准库的 json 包。该包通过一系列递归函数 encode 一个 Interface。如果在遍历的过程中出现错误,panic 会被触发,再由顶层的调用点捕获,接着返回一个合理的 error(参阅 encodeState的error
和marshal
方法)。
Go 标准库的约定是,如果一个包内部使用 panic,其对外的 API 仍然会返回一个明确的 error。
其他的 defer 的使用场景除了上文的释放文件资源,还有释放锁:
mu.Lock()
defer mu.Unlock()
打印footer:
printHeader()
defer printFooter()
等等。
总之,defer 语法(不管有没有 panic 和 recover)提供了一种不寻常且强大的控制流的机制。
参考资料
[1] https://go.dev/blog/defer-panic-and-recover
作者介绍