背景
随便使用Goroutine如同随地大小x,使用Goutine时愈发觉得没有安全感,这种感觉延伸到使用三方依赖为什么这么说?源于Go针对panic的处理方式,直接看代码:
func main() {
fmt.Println("main running...")
defer func() {
if err := recover(); err != nil {
fmt.Printf("%v\n", err)
}
}()
go doSomething()
time.Sleep(5 * time.Second)
fmt.Println("main end...")
}
func doSomething() {
fmt.Println("do something...")
panic("do something panic")
}
main中的recover是否能捕获到doSomething中的panic呢,答案是不行,然后呢?然后程序就崩溃了,只能在doSomething中recover,例如这样:
func doSomething() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("%v\n", err)
}
}()
fmt.Println("do something...")
panic("do something panic")
}
如果调用的方法使用了goroutine而它可能存在panic的风险我们该如何避免呢,倘若这个方法自己可以修改那就到goroutine方法中增加recover,如果不能修改呢,例如第三方依赖,那就自求多福吧
深入探究
go build -gcflags=-S .\main.go
使用上面的命令查看汇编代码,着重查看panic和recover
...
0x0067 00103 (/main.go:17) LEAQ main.doSomething·f(SB), AX
0x006e 00110 (/main.go:17) CALL runtime.newproc(SB)
...
调用runtime.newproc创建goroutine
recover
...
0x000e 00014 (/main.go:25) LEAQ +88(FP), AX
0x0013 00019 (/main.go:25) PCDATA $1, $0
0x0013 00019 (/main.go:25) CALL runtime.gorecover(SB)
0x0018 00024 (/main.go:25) TESTQ AX, AX
0x001b 00027 (/main.go:25) JEQ 84
...
recover实际上为runtime.gorecover,我们跟踪下这个方法,在runtime/panic.go中
func gorecover(argp uintptr) any {
// 获取当前goroutine
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然后将_panic结构体的recovered字段标记为true
panic
...
0x0065 00101 (/main.go:30) LEAQ type:string(SB), AX
0x006c 00108 (/main.go:30) LEAQ main..stmp_3(SB), BX
0x0073 00115 (/main.go:30) CALL runtime.gopanic(SB)
0x0078 00120 (/main.go:30) XCHGL AX, AX
0x0079 00121 (/main.go:30) CALL runtime.deferreturn(SB)
0x007e 00126 (/main.go:30) ADDQ $72, SP
0x0082 00130 (/main.go:30) POPQ BP
0x0083 00131 (/main.go:30) RET
0x0084 00132 (/main.go:30) NOP
...
可以看到panic其实是runtime.gopanic,跟踪到runtime包下的gopanic方法,在runtime/panic.go中
//go:linkname gopanic
func gopanic(e any) {
...
// 获取当前goroutine
gp := getg()
...
// 执行所有defer
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}
...
// 关键所在
fatalpanic(&p) // should not return
*(*int)(nil) = 0 // not reached
}
继续看下defer方法:nextDefer
func (p *_panic) nextDefer() (func(), bool) {
gp := getg()
...
if p.recovered {
mcall(recovery)
...
}
...
}
如果当前goroutine的_painc中recovered为true执行mcall,熟悉GMP模型应该知道mcall就是放弃执行权交还给g0,恢复时执行recovery函数
mcall switches from the g to the g0 stack and invokes fn(g)
从这里可以看出gorecover设置recovered的作用了,ok,我们找到了recover然后程序继续运行原因了,如果我们没有recover呢?
看下fatalpanic
//go:nosplit
func fatalpanic(msgs *_panic) {
...
gp := getg()
...
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}
exit ?
func exit(code int32) {
...
stdcall1(_ExitProcess, uintptr(code))
}
ExitProcess ? 是的,直接结束进程
小结
1. recover只在当前goroutine有效2. 任何一个goroutine中的panic都会导致整个程序退出