当数组越界、访问非法空间或者我们直接调用panic时,panic会停掉当前正在执行的程序,包括所有协程,比起exit直接退出,panic的退出更有秩序,他会他会先处理完当前goroutine已经defer挂上去的任务,执行完毕后再退出整个程序。
而defer的存在,让我们有更多的选择,比如在defer中通过recover截取panic,从而达到try…catch的效果
panic还可以接收一个参数,通常是字符串类型错误信息,执行到panic时,他会打印这个字符串和触发他的调用战。
当然,我们在写代码时要注意,不是所有的异常都能被捕获到的,向fatal error 和runtime.throw 都是不能被recover的
情况一:程序终止的原因
func main() {
panic("hello panic")
}
➜ learngo git:(master) ✗ go run main.go
panic: hello panic
goroutine 1 [running]:
main.main()
/Users/luojilab/go/src/learngo/main.go:109 +0x39
exit status 2
情况二:程序未被终止的原因
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("recover:%v\n", err)
}
}()
panic("hello panic")
}
➜ learngo git:(master) ✗ go run main.go
recover:hello panic
情况三:没有defer程序会终止吗?
func main() {
if err := recover(); err != nil {
fmt.Printf("recover:%v\n", err)
}
panic("hello panic")
}
➜ learngo git:(master) ✗ go run main.go
panic: hello panic
goroutine 1 [running]:
main.main()
/Users/luojilab/go/src/learngo/main.go:109 +0x39
exit status 2
情况四:写在goroutine里呢?
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("recover:%v\n", err)
}
}()
}()
panic("hello panic")
}
➜ learngo git:(master) ✗ go run main.go
panic: hello panic
goroutine 1 [running]:
main.main()
/Users/luojilab/go/src/learngo/main.go:109 +0x39
exit status 2
剖析panic的内部实现
首先,panic的基本单元是如下结构:
type _panic struct {
argp unsafe.Pointer //一个指针,指向defer调用的参数的指针
arg interface{} //panic传入的参数
link *_panic //是一个链表结构,指向上一个调用的_panic
recovered bool //是否已被recover 恢复
aborted bool //panic是否被强行中止
}
func main() {
panic("hello panic")
}
➜ learngo git:(master) ✗ go tool compile -S main.go
"".main STEXT size=66 args=0x0 locals=0x18
0x0000 00000 (main.go:108) TEXT "".main(SB), ABIInternal, $24-0
0x0000 00000 (main.go:108) MOVQ (TLS), CX
0x0009 00009 (main.go:108) CMPQ SP, 16(CX)
...
0x002f 00047 (main.go:120) PCDATA $2, $0
0x002f 00047 (main.go:120) MOVQ AX, 8(SP)
0x0034 00052 (main.go:120) CALL runtime.gopanic(SB)
0x0039 00057 (main.go:120)
显然,panic这里是调了gopanic这个函数,我们来看下该函数内部是如何实现的
// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
gp := getg() //获取指向当前goroutine的指针
...
//初始化一个panic的基本单元_panic
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//遍历当前goroutinue的defer链表
for {
//获取当前goroutinue上挂的defer
d := gp._defer
if d == nil {
break
}
//下面是一些对此defer的判断和处理,是否开放,是否可执行等
...
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
if !d.openDefer {
// For open-coded defers, we need to process the
// defer again, in case there are any other defers
// to call in the frame (not including the defer
// call that caused the panic).
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
}
...
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
//当前goroutinue若存在有效的defer调用,
//就会调下面的reflectcall方法来处理defer后面的操作,
//当然,若defer里面进行了recover处理,则会调用gorecover函数,
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
}
p.argp = nil
//在pc、sp中记录当前defer的pc、sp
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
...
//已经有recover被调用
if p.recovered {
...
gp._panic = p.link
...
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
//移除那些被终止的panic
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
...
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 恢复,调用recovery
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
preprintpanics(gp._panic)//准备panic程序退出时要打印的参数
//递归打印所有panic中的参数信息,并执行exit(2)来退出程序
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
由上,panic就是处理当前goroutinue上的panic链表,以及根据其上挂载的defer链表。
...
0x0024 00036 (main.go:111) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:111) PCDATA $2, $0
0x002b 00043 (main.go:111) MOVQ AX, 8(SP)
0x0030 00048 (main.go:111) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:111) TESTL AX, AX
...
0x004b 00075 (main.go:116) MOVQ AX, 8(SP)
0x0050 00080 (main.go:116) CALL runtime.gopanic(SB)
0x0055 00085 (main.go:116) UNDEF
0x0057 00087 (main.go:111) XCHGL AX, AX
0x0058 00088 (main.go:111) CALL runtime.deferreturn(SB)
0x005d 00093 (main.go:111) MOVQ 16(SP), BP
...
0x0026 00038 (main.go:112) MOVQ AX, (SP)
0x002a 00042 (main.go:112) CALL runtime.gorecover(SB)
...
0x0072 00114 ($GOROOT/src/fmt/print.go:208) LEAQ go.string."recovered:%v\n"(SB), AX
...
0x00a3 00163 ($GOROOT/src/fmt/print.go:208) CALL fmt.Fprintf(SB)
看到这里,我们发现有四个关键的方法
runtime.deferproc
runtime.gopanic
runtime.deferreturn
runtime.gorecover
上面我们知道在panic时调用gopanic函数,接着遍历当前goroutinue上挂载的defer链表,在执行reflectcall时碰到recover,就执行gorecover函数处理
gorecover是如何恢复panic的呢?
// The implementation of the predeclared function recover.
// Cannot split the stack because it needs to reliably
// find the stack segment of its caller.
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
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
}
检查当前goroutinue是否在panic的流程中,在的话就将panic的recovered字段置微为true,否则直接返回nil,所以说recover在普通的流程被调用时时没有任何作用的。
gorecover函数仅仅改了recovered标记,那么gouroutine是怎么从panic返回的呢?
reflectcall调用完gorecover后会在pc、sp中记录defer的pc、sp,之后会调用
recovery
// Unwind the stack after a deferred function calls recover
// after a panic. Then arrange to continue running as though
// the caller of the deferred function returned normally.
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// d's arguments need to be in the stack.
if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {
print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "]\n")
throw("bad recovery")
}
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
//切换上下文
gogo(&gp.sched)
}
recovery更改了当前goroutinue的上下文,使它指向上面defer的位置,最后的gogo函数就直接切到了defer的上下文中
defer
//runtime/runtime2.go
type _defer struct {
siz int32 //参数大小
started bool // defer是否被调用过的标识
sp uintptr // sp at time of defer
pc uintptr
fn *funcval // defer 后面跟的function
_panic *_panic // 指向panic链表
link *_defer // 指向defer链表
...
}
func deferproc(siz int32, fn *funcval) {
...
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
//newdefer从_defer池中找是否有可复用的_defer基本单元,若没有,就会malloc一个新的
//并且将当前goroutinue的defer指向这个d, 新的defer总是会出现在最前面
//这就是defer先入后出属性的原因所在,
d := newdefer(siz)
...
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()
}
return0总是会检查返回值,若为0的话,说明deferproc成功,可以继续处理后面的逻辑,当返回值为非0的情况时(就是panic恢复时从recovery函数跳转过来的),此时就会跳转到函数的最后,return之前,继续执行deferreturn。由于调用recover的defer已经从defer链表上摘掉了,所以可以继续执行之前没完成的defer,并最终返回当前函数的调用者。
总之,deferproc这个函数就是初始化了一个defer结构,最后通过特殊的返回方式来结束函数调用,通过检查返回值为0或1使其分流至不同的流程中。
到这里,我们应该了解了panic是如何通过recover恢复的
当一个 panic 被恢复后,调度并因此中断,会重新进入调度循环,进而继续执行 recover 后面的代码, 包括比 recover 更早的 defer(因为已经执行过得 defer 已经被释放, 而尚未执行的 defer 仍在 goroutine 的 defer 链表中),或者 recover 所在函数的调用方。
我们接着看下deferreturn函数都做了什么事情
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
//会不断地进入 deferreturn 函数,判断链表中是否还存着 _defer。
//若已经不存在了,则返回,结束掉它。
//简单来讲,就是处理完全部 defer 才允许你真的离开它
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
在一个函数中调用过 defer 关键字,那么编译器将会在结尾处插入 deferreturn 方法的调用。
defer涉及的两个关键函数deferproc和deferreturn,后者只要函数调用defer关键字,就会在结束之前插入deferreturn来完成defer后面的操作。
defer的性能问题
defer 关键字其实涉及了一系列的连锁调用,内部 runtime 函数的调用
编译期间,就需要做不少事情
deferproc中defer函数的注册阶段,_defer的生成以及初始化,链表结构的挂载,还涉及获取目标函数的地址,传入函数参数。
deferreturn时也有各种释放defer和jmpdefer的逻辑操作。
测试一下:
func HasDefer(a, b string) {
defer func(a, b string) {
_ = a + b
}(a, b)
}
func NotDefer(a, b string) {
_ = a + b
}
基准测试:
func BenchmarkHasDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
HasDefer("has", "defer")
}
}
func BenchmarkNotDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
NotDefer("not", "defer")
}
}
➜ learngo git:(master) ✗ go test -bench=. -benchmem -run=none
goos: darwin
goarch: amd64
pkg: learngo
BenchmarkHasDefer-4 20000000 81.8 ns/op 0 B/op 0 allocs/op
BenchmarkNotDefer-4 50000000 24.0 ns/op 0 B/op 0 allocs/op
PASS
ok learngo 2.968s
可以看到,一个 defer 关键字实际上包含了不少的动作和处理,代码中使用defer, 可能会给程序的性能代码几十纳秒的开销(根据运行环境的不同,数值有所不同)。
通常,我们在资源控制或者释放锁 的处理后面都会加上defer + close() ,或者defer unlock,若后面的处理逻辑非常多,当并发流量很大时,资源和锁迟迟得不到释放也会有不少问题。
当然,defer 最大的功能是 Panic 后依然有效。如果没有 defer,Panic 后就会导致 unlock 丢失,最终会死锁,
所以,对于defer,我们该用就用,应该及时关闭就最好不要延迟。
Go1.13中defer的性能有所提升,
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool //标示defer是在堆上还是栈上分配
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer
}
runtime.deferprocStack()存在d.heap = false的操作
runtime.deferproc()还存在
编译器会根据应用场景去选择使用 deferproc 还是 deferprocStack 方法,他们分别是针对分配在堆上和栈上的使用场景,对于有隐式循环和显式循环的场景,会分配在堆上调用前者
总之,其defer的堆栈分配规则的改变,要想享受defer带来的30%性能提升,就要避免下面两种场景的代码
defer语句外层嵌套有显式循环;
for循环,哪怕只循环了一次,都会调用runtime.deferproc()从堆上分配
defer语句有隐式循环;
在defer中存在goto关键字
Go1.14也对defer有了进一步优化,新加入了开放编码(Open-coded)defer类型,编译器在ssa过程中会把被延迟的方法直接插入到函数的尾部,避免了运行时的deferproc及deferprocStack操作
在deferreturn流程上进行了优化,之前都是通过jmpdefer一遍一遍的调用deferreturn来处理defer链上的所有延迟函数,直到处理完所有的defer会退出,1.14中改成了for循环去处理,省掉了很多函数调用,参数传参的消耗
条件是open-coded最多支持8个defer,超过则取消