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时计算的。
- 将当前_defer结构体添加到goroutine的_defer链表最前面,而_defer的执行顺序是从前往后的,因此程序中后调用的defer函数先执行。
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的入参信息【采用递归方法从后向前打印,由于采用头插法插入,故输出效果为先入先出】,最后终止当前进程(注意不是线程、也不是协程,而是进程被终止)
2.2.3 程序恢复
recover关键字会在编译时转换成runtime.gorecover方法,会将goroutine的首个_panic的recovered字段置为true,在runtime.gopanic函数中每次执行完一个_defer函数后会判断该字段,若为true,才执行真正的恢复过程,即使用runtime.gogo方法返回到defer调用recover方法的位置,继续defer链的执行
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
}