通常 panic 和 recover 是用来处理异常问题的。我们来综述下,他们各自的特点:
painc 可以是系统出现严重错误时产生,也可以人为调用painc函数;如果不加处理,painc会沿着调用栈层层上报,直到程序崩溃终止。
recover 可以回收 panic,返回一个空接口,但是必须要在defer 中才行。
func main(){
defer println("in main")
go func() {
defer println("in goroutine")
panic("panic")
}()
time.Sleep(1 * time.Second)
}
in goroutine
panic: panic
...
exit status 2 //panic 一般都是错误码 2
可以注释掉这些defer ,看下他的打印结果,很直观。
在Goroutine中产生了panic,只会执行当前Goroutine中的defer方法,不会触发main中的(实际上,按照官方文档的解释,go func() 本身就是这个Goroutine 的main 函数)。如果把Goroutine中的defer方法注释掉,依然不会触发main中的defer,系统还是会崩溃。原因是:defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。
//源代码在 go/src/runtime/runtime2.go 899行
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}
根据注释,我们大致可以清楚:
编译器会把painc 转化为 gopanic 函数。具体的逻辑会在这个方法里执行。我这里只把里面的部分代码拿出来。这里面有非常详细的注释,很方便阅读。
// 代码在 go/src/runtime/panic.go 887 行
func gopanic(e interface{}) {
gp := getg()
//创建一个新的painc 并把他加到链表的最前端
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//循环defer链表, 并调用延迟函数。
for {
d := gp._defer
...
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
freedefer(d)
if p.recovered {
...
}
...
}
...
fatalpanic(gp._panic) // 最终处理方法
}
接下来,看下fatalpanic函数的情况:
// 代码在 go/src/runtime/panic.go 1187 行
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs) //打印出全部的panic信息,包括调用信息
}
docrash = dopanic_m(gp, pc, sp) //这里这方法很迷惑,不过看起来是针对goruntine 进行的操作
})
if docrash {
crash()
}
systemstack(func() {
exit(2) //如果panic没有被恢复,那么就会在这里退出程序,错误码 2
})
}
Recover 函数比较简单,这个东西就是用来处理Panic的没有其他的用途。我们直接看下源码:
// 代码在 go/src/runtime/panic.go 1082 行
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
//这里只有存在Painc的时候,才会运行。
p.recovered = true
return p.arg
}
//如果当前goruntine里没有panic 直接返回nil
return nil
}
这里的逻辑是把_panic里的recovered设置成true,没有多余操作。之后回到gopanic函数中,经过简单处理跳转到recovery中。再往后就是 defer的逻辑,一直走到deferreturn,回到正常的逻辑中。
到这里基本可以总结一下Panic和Recover的一些特点。
通过学习源码,基本上解释了“panic 详情会在控制权传播的过程中,被逐渐地积累和完善,并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。”
Defer,人如其名,延迟执行函数。延迟到什么时候呢?这要延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么。注意:
Defer的底层原理非常非常的复杂,我们这里只做一些简单描述和调用逻辑说明,具体的过程可以参看最后的连接或者直接阅读源码。
//源代码在 go/src/runtime/runtime2.go 861行
type _defer struct {
siz int32 //参数和返回结果的内存大小
started bool
heap bool
openDefer bool //是否开放编码优化
sp uintptr //栈指针计数器
pc uintptr //程序计数器
fn *funcval //实际延时的函数体 可以是空值
_panic *_panic //触发defer 的panic 可以是空的
link *_defer //指向下一个defer
//在开放编码优化的情况下,会使用到这两个字段
fd unsafe.Pointer
varp uintptr
//在堆栈模式下,会用到这个字段
framepc uintptr
}
通过观察,首先可以确认defer 仍然是一个链表结构,会通过link串起来。其次,直观的反映了defer 优化过程:
这一部分的逻辑非常复杂,建议看下 GO语言设计实现,写的很清楚,配合源码使用。
我们在这里只取过程和结论:
问题引申,堆上分配和栈上分配的区别?GO 语言逃逸分析看内存分配
func main(){
a := 1
b := 2
defer calc(a, calc(a,b,"0"),"1")
a = 0
defer calc(a, calc(a,b,"3"),"2")
}
func calc(x,y int,s string) int{
fmt.Println(s)
fmt.Println(x,y,x+y)
return x+y
}
返回的结果:
0
1 2 3
3
0 2 2
2
0 2 2
1
1 3 4
这里考察两个点:
这个现象就充分解释了一开始说的关于defer的知识点。关于第二点说的详细一下:
调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算
这里直接引用 郝林的回复:
这是两种完全不同的异常处理机制。Go语言的异常处理机制是两层的,defer和recover可以处理意外的的异常,而error接口及相关体系处理可预期的异常。Go语言把不同种类的异常完全区别对待,我觉得这是一个进步。 另外,defer机制能够处理的远不止异常,还有很多资源回收的任务可以用到它。defer机制和goroutine机制一样,是一种很有效果的创新。 我认为defer机制正是建立在goroutine机制之上的。因为每个函数都有可能成为go函数,所以必须要把异常处理做到函数级别。可以看到,defer机制和error机制都是以函数为边界的。前者在函数级别上阻止会导致非正常控制流的意外异常外溢,而后者在函数级别上用正常的控制流向外传递可预期异常。 不要说什么先驱,什么旧例,世界在进步,技术更是在猛进。不要把思维固化在某门或某些编程语言上。每种能够流行起来的语言都会有自己独有的、已经验证的语法、风格和哲学。
答案是当然可以了,如果理解了上边的原理的话,就能详细解释了,panic是链状的,后触发的panic会添加到最前端,循环调用defer的时候就会处理最前面的那个panic(可以理解为后发先至)。出现的现象就是,defer外的panic会被里面的替换掉。同样的,你甚至可以在defer中调用defer!
func main(){
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic: %s\n", p) }
}()
defer func() {
//defer func() {
//if p := recover(); p != nil {
// fmt.Printf("panic: %s\n", p) }
//}()
panic("panic in defer")
}()
panic("panic in main")
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。