理解 Go defer, panic, recover
Defer
defer 语句会将函数调用放入一个列表. 当外围的函数执行返回后列表内保存的函数调用会被执行. defer 像 Python 的 with, finally 一样都具有类似资源情理的作用. 当然 defer 在这方面更灵活.
defer 的行为遵循 3 个简单的原则:
一个 defer 函数的参数在放入待调用列表的时候被立即求值. 比如:
func a() {
i := 0
defer fmt.Println(i) // @1
i++
return
}这个例子中, @1 中的 i 会被立即求值, 所以 a 函数调用返回时, 会打印出 “0” 而不是 “1”.
defer 函数在外围函数返回时遵循 先进后出 的调用顺序.
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}b 函数将会打印出 “3210”.
defer 函数会读取并且赋值外围函数的命名返回值(named return values).
func c() (i int) {
defer func() { i++ }()
return 1
}这个例子中 i 的值会 在 defer 被调用时执行加 1 操作, 所以 c 将返回 2. 这对于修改一个函数的 error 返回值很方便.
Panic
panic 是一个内置的函数用来阻断一般的控制流程. Go spec 对于 panic 有一段描述:
当执行函数 F 时, 一个明确的 panic 调用或者运行时 panic(数组越界访问等)会终止 F 的执行. F 所属的任何 defer 函数继续执行. 紧接着, 任何 F 调用者的 defer 函数运行, 而且对于执行中的 goroutine 中的任何顶层 defer 函数也是如此. 到那时, 程序终止并且报错(错误包含在 panic 参数中). 该终止序列被称为 panicking.
Recover
recover 是一个内建函数用来恢复一个 panicking 的 goroutine. Go spec 对于 recover 有一段描述:
设想有一个函数 G, 有一个 defer 函数会调用 recover 并且和 G 在同一 goroutine 的函数发生了 panic. 当运行到 defer 函数 D 调用的时候, D 调用 recover 的返回值是 panic 调用时传入的值. 如果 D 正常返回, 没有开启一个新的 panic, panicking 序列会终止. 在这种情况下, G 和 panic 之间的函数调用状态被废弃, 并且正常的调用恢复. 然后 G 中任何在 D 之前的 defer 函数会执行, 并且 G 的执行通过返回到它的调用者而终止.
以下情况中 recover 的返回值为 nil:
- panic 参数为 nil;
- 当前 goroutine 没有发生 panicking;
- recover 没有被一个 defer 函数直接调用.
以下示例程序演示了 panic 和 defer 的原理:
package main |
函数返回:
Calling g. |
如果我们移除 f 中 defer 将输出:
Calling g. |
Go 标准库中的约定是, 即使一个包在内部使用 panic, 其外部 API 仍会给出明确的错误返回值(真实的例子可以参见标准库 json 包). 这是很好的使用原则, 尤其当我们写自己库的时候, 难免需要处理来自第三方的包返回的各种 error, 如果不需要处理, 简单的一个 panic 会是很好的方案.
结束
到这里可能就是你所能看到的关于 defer, panic 和 recover 的所有”坑”(我一向不觉得文档中有的你没看到的东西叫坑). 下面的内容可以不看了, 但是你看了, 保证没坏处.
当然可能有人说 recover 不能恢复所有的 panic, 比如如下的程序:
package main |
如果运行该程序, 那么会得到如下的输出:
fatal error: concurrent map writes |
其实这个不叫 panic, 按官方文档的说法叫: 崩溃(crash) 和 内存腐败(memory corruption). 由于程序无视 Go 内存模型 而导致的数据竞争(data race).
当然对于以上程序来说, 运行程序崩溃是显而易见的, 然而有的程序运行时不会显式的的崩溃, 如:
package main |
输出:
2 b |
为了检测和预防这种崩溃, Go 内建了数据竞争探测器(data race detector), 可以通过传递 flag: -race 给 go 命令来启用:
$ go test -race mypkg |
我们再次对上面的程序使用命令: go run -race main.go, 那么数据探测器可以检测到:
================== |