searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

golang中的defer、recover与panic

2023-10-25 09:01:29
15
0
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
}
0条评论
0 / 1000
l****n
3文章数
0粉丝数
l****n
3 文章 | 0 粉丝
l****n
3文章数
0粉丝数
l****n
3 文章 | 0 粉丝
原创

golang中的defer、recover与panic

2023-10-25 09:01:29
15
0
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
}
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0