在各种 go 文档和入门书中,都会要求 recover 函数一定要在 defer 中调用,否则 panic 的时候无法捕获,可是为什么一定要在 defer 中调用才行?defer panic 和 recover 之前有什么联系?
在看完了相关的源码之后,我试着来解释一下defer 、panic、recover 的原理和联系,以及我从一开始就困惑的问题:为什么 recover 一定要在 defer 中调用
以下不会大段的粘贴和讲解代码,只会挑其中最重要的几个标识来辅助解释
defer
对于 defer 来说,编译器会把 defer 关键字转换成 deferproc 函数和 deferreturn 函数,deferproc 和 deferreturn 是成对出现的,只不过 deferproc 出现在 defer 关键字出现的地方,deferreturn 则会被插入到函数的末尾。
deferproc 是用来生成 defer 的结构体,新生成的 defer 会按照后进先出的规则绑定在当前 g 的 defer 链表上, defer 后面的函数对应的是 defer.fn 属性。而 deferreturn 会取当前 g 的 defer 链表头部的 defer ,并且执行 defer 的 fn 函数,执行之前会释放 defer ,并且把 defer 从当前 g 的 defer 链表中删除。
panic
对于 panic 来说,编译器会把 panic 关键字转换成 gopanic 函数,gopanic 会新建一个 panic 结构体,同样以后进先出的规则绑定到当前 g 的 panic 链表上(也就是放在头部),并且遍历当前 g 的 defer 链表。
recover
对于 recover 来说,编译器会把 recover 关键字转换成 gorecover 函数,该函数主要是把当前 g 的 panic 列表头部的 panic 结构体的 recovered 属性设置为 true
为什么 recover 必须在 defer 中调用才能捕获 panic
这里需要继续介绍 gopanic 函数,gopanic 遍历当前 g 的 defer 链表,取出 defer,执行 defer.fn。然后判断上面新建的 panic 的 recovered 属性是否为 true,为 true 则执行恢复操作,为 false 则继续遍历执行 defer.fn 直到把所有 defer 都执行完成,然后打印 panic 信息并且退出程序。
上面有一个很关键的步骤,在执行 defer.fn 之后判断当前 panic 的 recovered 属性,这里就要求 recover 函数必须要在 defer 中了,不然 panic.recovered 永远为 false。假如当前 fn 函数中调用了 recover,此时 gorecover 函数就会把当前 g 的 panic 链表头部的 panic.recovered = true,并且把 panic的错误信息返回给调用 recover 的调用方。而 panic 链表头部的 panic 就是上面的 panic。
panic 的 recovered 为 true,所以会恢复当前 g 的执行,而恢复的位置为函数末尾的 deferreturn,在 gopanic 执行过的 defer 都会从当前 g 的 defer 链表中删除,所以 deferreturn 不会重复执行 defe。
同样,包含 recover 的 defer 函数的位置也不会影响其他 defer 函数的执行。因为不管包含 recover 的 defer 函数位置在哪,只要程序 panic,gopanic 都会遍历当前 g 的 defer 链表,遇到包含 recover 的 defer 函数时。又会跳到函数末尾执行deferreturn(所以不会执行 panic 之后的逻辑),所以那些没有在 gopanic 中执行的 defer 函数,又会被执行,依然是按照 defer 链表后进先出的规则。
也就是说,包含 recover 的 defer 只是用来恢复当前 g 的执行,不会改变 defer 链表的顺序(理所当然),所以不管包含 recover 的 defer 函数在 defer 链表中哪个位置,都不会影响其他的 defer 函数执行。