go中defer/panic/recover的纠葛
1. defer
1.1 作用与应用场景
在函数调用结束后,完成一些收尾操作,例如数据库回滚、关闭文件、关闭数据库链接等等
1.2 基本原则
-
defer函数参数会被预计算
-
多个defer执行顺序是先入后出的
-
defer中可以改变命名返回变量的值
1.3 原理
golang中defer的实际结构体如下:
type _defer struct {
siz int32 //参数和结果的内存大小
started bool
openDefer bool //当前 defer 是否经过开放编码的优化
sp uintptr //栈指针
pc uintptr //调用方的程序计数器
fn *funcval //defer 关键字中传入的函数
_panic *_panic //触发延迟调用的结构体,可能为空
link *_defer //将_defer结构体串联成一个链表
}
golang运行时有两个重要的defer机制入口:
-
runtime.deferproc
负责创建新的延迟调用,go编译器会将defer自动转为该方法 -
runtime.deferreturn
负责在函数调用结束时执行所有的延迟调用,go编译器会在所有调用defer的函数末尾添加上该方法
golang为每一个goroutine维护了一个延迟调度队列,即_defer链表。
在runtime.deferproc方法中,会构建一个新的runtime._defer结构体,设置fn、sp、pc等参数,并完成以下操作:
-
拷贝defer函数的参数到相邻空间,由此可知,defer函数的参数是在执行到defer语句时计算的,而不是在函数结束后调用defer时计算的。
func deferproc(siz int32, fn *funcval) { gp := getg() ... sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() // the arguments of fn are in a perilous state. The stack map // for deferproc does not describe them. So we can't let garbage // collection or stack copying trigger until we've copied them out // to somewhere safe. The memmove below does that. // Until the copy completes, we can only call nosplit routines. // 为了防止fn的参数被垃圾回收,或者栈拷贝(?),需要提前将其拷贝到安全的位置 d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.link = gp._defer gp._defer = d d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() }
-
将当前_defer结构体添加到goroutine的_defer链表最前面,而_defer的执行顺序是从前往后的,因此程序中后调用的defer函数先执行。
func deferproc(siz int32, fn *funcval) { gp := getg() ... d := newdefer(siz) ... d.link = gp._defer gp._defer = d ... }
2. panic与recover
2.1 应用场景
程序遇到panic后,会立刻停止后续程序的执行,并进入当前goroutine的延迟调用队列,递归执行defer
recover是一个只能在defer中发挥作用的函数,可以终止由panic导致的程序崩溃
2.2 基本现象与原理
在go源码中,panic是用结构体runtime._panic表示的:
type _panic struct {
argp unsafe.Pointer
arg interface{} //panic函数传入的参数
link *_panic //指向更早调用的_panic结构
recovered bool //表示当前_panic是否正确调用了recover恢复
aborted bool //表示当前panic是否被终止
pc uintptr
sp unsafe.Pointer
goexit bool
}
2.2.1 程序崩溃
go编译器会将panic关键字转换为runtime.gopanic函数,在该函数下执行:
- 创建新runtime._panic结构,将自己添加到goroutine的_panic链表最前面
- 获取当前goroutine的_defer链表,依次循环执行
- 执行完所有的defer之后,调用runtime.fatalpanic函数,打印当前goroutine下_panic链表中所有_panic的入参信息【采用递归方法从后向前打印,由于采用头插法插入,故输出效果为先入先出】,最后终止当前进程(注意不是线程、也不是协程,而是进程被终止)
func gopanic(e interface{}) {
gp := getg()
...
//第一步, 创建新runtime._panic,将自己添加到goroutine的_panic链表最前面
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
//第二步,循环执行所有defer
for {
d := gp._defer
if d == nil {
break
}
//嵌套panic的处理
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
...
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
done := true
if d.openDefer {
...
} else {
//执行defer函数
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
}
p.argp = nil
...
if p.recovered {
//真正执行程序恢复的地方
...
}
}
//第三步,打印panic信息并终止进程
preprintpanics(gp._panic)
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
2.2.3 程序恢复
recover关键字会在编译时转换成runtime.gorecover方法,会将goroutine的首个_panic的recovered字段置为true,在runtime.gopanic函数中每次执行完一个_defer函数后会判断该字段,若为true,才执行真正的恢复过程,即使用runtime.gogo方法返回到defer调用recover方法的位置,继续defer链的执行
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
func gopanic(e interface{}) {
...
for {
d := gp._defer
if d == nil {
break
}
...
if p.recovered {
gp._panic = p.link
if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
// A normal recover would bypass/abort the Goexit. Instead,
// we return to the processing loop of the Goexit.
gp.sigcode0 = uintptr(gp._panic.sp)
gp.sigcode1 = uintptr(gp._panic.pc)
mcall(recovery)
throw("bypassed recovery failed") // mcall should not return
}
...
}
}
...
}
3. 常见问题
3.1 为什么recover函数要包裹在defer的func()中?
//以下是一种无效调用recover()的例子:
func main(){
defer recover() //无效
panic(1)
}
//正确使用recover()的例子:
func main(){
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic(1)
}
这是因为recover()会被编译器转换为gorecover()函数:
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
当程序遇到defer时,defer函数的参数会预先计算并拷贝,故传入的参数argp会与实际执行gorecover时的p.argp不一致,导致程序直接返回nil,故无法在runtime.gopanic函数中正确地恢复程序。
因此,需要使用内联函数的方式才能正确地调用recover()恢复panic。
3.2 触发panic的协程是如何停止所有协程的?
panic在编译后执行runtime.gopanic函数,执行完defer链之后,若未执行recover(),则会执行runtime.fatalpanic函数,其中调用了exit(2),退出进程,从而其他的goroutine都会同时被终止.
func fatalpanic(msgs *_panic) {
...
systemstack(func() {
exit(2) //终止当前进程
})
*(*int)(nil) = 0 // not reached
}
3.3 panic触发之后defer函数里再次触发新的panic,会发生什么?
分几种情况:
(1)panic被recover捕捉恢复
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("panic3")
}()
defer func() {
panic("panic2")
}()
panic("panic1")
}
//输出: panic3
(2)panic没有被recover
func main() {
defer fmt.Println("in main")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()
panic("panic once")
}
/*
输出:
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
*/
还有一种情况,panic被recover之后,后续的defer中又出现一个panic
func main() {
defer func() {
panic("panic3")
}()
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("panic2")
}()
panic("panic1")
}
/*
输出:
panic2
panic: panic3
goroutine 1 [running]:
...
exit status 2
*/
解释以上现象涉及两个知识点:
(1) panic被defer中的recover函数恢复之后,程序会继续defer链的执行
(2) 嵌套panic的时候,会重复调用某个defer,defer上会标记started为true,表示已经被调用过,此时抛弃之前的panic,因此后续再进行Recover时,只能拿到最新的panic信息
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
...
}
// Mark defer as started, but keep on list, so that traceback
// can find and update the defer's argument frame if stack growth
// or a garbage collection happens before reflectcall starts executing d.fn.
d.started = true
参考文章
[1] https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover
[2] https://blog.csdn.net/pengpengzhou/article/details/107663338