golang recover
使用
在 Golang 中可以通过 recover 来捕获 panic 恢复程序,使用时需要注意:
-
recover 只能捕获 当前协程 的 panic,类似下面的方式捕获会失败:
package main import ( "log" ) func main() { // Recover outside a goroutine defer func() { log.Println(recover()) }() go func() { // Panic occurs in a goroutine panic("A bad boy stole a server") }() }
-
recover 只在 deferred 函数中有效,间接 defer 调用也不会成功:
package main import ( "fmt" "log" "time" ) func getItem() { go func() { // Call Recover() Tool Function. defer func() { Recover("Panic in goroutine") }() // Panic here. panic("A bad boy stole the server") // Test the panic result. fmt.Println("Will NOT Reach Here") }() } func Recover(funcName string) { if err := recover(); err != nil { // If the panic is catched. log.Printf("panic para: %v, panic info: %v\n", funcName, err) } } func main() { getItem() // Fail to catch the panic, program will crash time.Sleep(time.Second) }
官方文档中的描述:
The return value of recover is nil if any of the following conditions holds:
- panic's argument was nil;
- the goroutine is not panicking;
- recover was not called directly by a deferred function.
当 pnaic 参数为 nil 时,虽然 recover 返回值也是 nil,但当前 panic 仍然会被被标记为 recovered,并恢复程序的执行。
原理
在 Golang 中程序运行的基本单位为 Goroutine,在 runtime 中每个 Goroutine 都对应一个结构体 g,在结构体 g 上存在有 panic 和 defer 链表:
type g struct {
_panic *_panic
_defer *_deder
}
当代码中出现 panic 会,会创建一个 _panic 结构绑定到当前 Goroutine 上并遍历当前 Goroutine 上绑定的 _defer 链表执行:
func gopanic(e interface{}) {
gp := getg()
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
d := gp._defer
if d == nil {
break
}
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
p.argp = unsafe.Pointer(getargp(0)) // 指向当前 defer 函数的参数列表指针
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
if p.recovered {
//...
}
}
fatalpanic(gp._panic)
*(*int)(nil) = 0
}
而在执行 defer 中的 recover 时,会判断当前 _panic 上的 argp 和当前 argp 是否相同:
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
因此,如果 recover 不在当前 Goroutine 的 deferred 函数中,那么就不会执行 p.recovered = true
语句, 而 runtime.gopanic 函数依赖于 _panic.recovered
的值来判断是否恢复代码的执行。