聊聊golang中的panic和defer

聊聊golang中的panic和defer

当数组越界、访问非法空间或者我们直接调用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是否被强行中止
}

在这里插入图片描述
当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链表。

  • 新的panic会被挂在当前goroutinue的panic链的最前面
  • 调用reflectcall循环执行defer链上的延迟函数
  • 没有recover的情况下执行完所有的defer会break跳出for 循环执行最后的fatalpanic来终止程序
       ...
        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链表
    ...
}

在这里插入图片描述
defer是面向编译器的声明

  1. 编译期间,会将defer声明编译成runtime.deferproc(fn),运行时调用此方法将所有defer挂载goroutinue的defer链上
  2. 在函数return之前,增加runtime.deferreturn()调用,处理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恢复的

  • 在gopanic函数中,执行defer链中所有的延迟函数,执行时遇到recover会使得_panic.recovered=true
  • 调用recovery通过_defer.sp和_defer.pc跳转到deferproc的上下文中来执行,同时将返回值设置为1
  • 最后由于deferproc的返回值不为0,根据return0的特殊逻辑会跳到函数末尾直接执行deferreturn,恢复到了正常的流程

当一个 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的调用信息
  • 通过freedefer释放该defer,放入deferpool中,便于复用
  • 调用jmpdefer跳转到defer后面的延迟函数处

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,超过则取消

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值