5.4 panic and recover

Kesa...大约 5 分钟golang

Golang 中的panic和recover:

  • panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer

  • recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用

golang-panic
golang-panic

5.4.1 现象

使用panic和recover时,会有:

  • panic 只会触发当前 Goroutine 的defer
  • recover 只有在 defer 中调用才会生效
  • panic 允许在 defer 中嵌套多次调用

跨协程失效

panic 只会触发当前 Goroutine 的延迟函数调用:

func main() {
	defer println("defer in main")
	go func() {
		defer println("defer in goroutine")
		panic("panic in goroutine")
	}()

	time.Sleep(time.Second)
}
$ go run main.go
defer in goroutine
panic: panic in goroutine

goroutine 5 [running]:
main.main.func1()

可以看到main函数中的defer没有被触发,只有panic所在的goroutine的defer被触发。

因为runtime.deferprocopen in new window 会将延迟调用函数与调用方所在 Goroutine 进行关联,所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数。

golang-panic-and-defers
golang-panic-and-defers

失效的recover

recover只有在defer中才会生效:

func main() {
	defer println("defer in main")
	if err := recover(); err != nil {
		println(err)
	}
	panic("panic in main")
}

$ go run main.go
defer in main
panic: panic in main

goroutine 1 [running]:
main.main()

上述代码因为recover在panic调用之前被调用了,不会生效。需要在defer中才会生效。

panic嵌套

panic可以嵌套:

func main() {
	defer fmt.Println("in main")
	defer func() {
		defer func() {
			panic("panic again and again")
		}()
		panic("panic again")
	}()

	panic("panic once")
}

$ go run main.go
in main
panic: panic once
	panic: panic again
	panic: panic again and again

5.4.2 数据结构

panic的数据结构由runtime._panicopen in new window表示:

type _panic struct {
	argp      unsafe.Pointer
	arg       interface{}
	link      *_panic
	recovered bool
	aborted   bool
	pc        uintptr
	sp        unsafe.Pointer
	goexit    bool
}
  • argp:指向defer调用时参数的指针
  • arg:调用panic的参数
  • link:指向更早调用的panic,形成链表
  • recovered:表示当前panic是否被恢复
  • aborted:表示当前panic是否被终止

5.4.3 panic

panic 终止程序的实现原理,编译器会将关键字 panic 转换成 runtime.gopanicopen in new window,函数主要流程:

  1. 创建新的 runtime._panicopen in new window 并添加到所在 Goroutine 的 _panic 链表的最前面
  2. 在循环中不断从当前 Goroutine 的 _defer 中链表获取 runtime._deferopen in new window 并调用 runtime.reflectcallopen in new window 运行延迟调用函数
  3. 调用 runtime.fatalpanicopen in new window 中止整个程序
func gopanic(e interface{}) {
	gp := getg()
	...
	var p _panic
	p.arg = e
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	for {
		d := gp._defer
		if d == nil {
			break
		}

		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))

		d._panic = nil
		d.fn = nil
		gp._defer = d.link

		freedefer(d)
		if p.recovered {
			...
		}
	}

	fatalpanic(gp._panic)
	*(*int)(nil) = 0
}

runtime.fatalpanicopen in new window 实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanicsopen in new window 打印出全部的 panic 消息以及调用时传入的参数:

func fatalpanic(msgs *_panic) {
	pc := getcallerpc()
	sp := getcallersp()
	gp := getg()

	if startpanic_m() && msgs != nil {
		atomic.Xadd(&runningPanicDefers, -1)
		printpanics(msgs)
	}
	if dopanic_m(gp, pc, sp) {
		crash()
	}

	exit(2)
}

打印崩溃消息后会调用 runtime.exitopen in new window 退出当前程序并返回错误码 2,程序的正常退出也是通过 runtime.exitopen in new window 实现的。

5.4.4 recover

编译器会将关键字 recover 转换成 runtime.gorecoveropen in new window:

func gorecover(argp uintptr) interface{} {
	gp := getg()
	p := gp._panic
	if p != nil && !p.recovered && argp == uintptr(p.argp) {
		p.recovered = true
		return p.arg
	}
	return nil
}

runtime.gorecoveropen in new window 函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanicopen in new window 函数负责:

func gopanic(e interface{}) {
	...

	for {
		// 执行延迟调用函数,可能会设置 p.recovered = true
		...

		pc := d.pc
		sp := unsafe.Pointer(d.sp)

		...
		if p.recovered {
			gp._panic = p.link
			for gp._panic != nil && gp._panic.aborted {
				gp._panic = gp._panic.link
			}
			if gp._panic == nil {
				gp.sig = 0
			}
			gp.sigcode0 = uintptr(sp)
			gp.sigcode1 = pc
			mcall(recovery)
			throw("recovery failed")
		}
	}
	...
}

runtime.recoveryopen in new window 在调度过程中会将函数的返回值设置成 1。

当 runtime.deferprocopen in new window 函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturnopen in new window:

func deferproc(siz int32, fn *funcval) {
	...
	return0()
}

跳转到 runtime.deferreturnopen in new window 函数之后,程序就已经从 panic 中恢复了并执行正常的逻辑,而 runtime.gorecoveropen in new window 函数也能从 runtime._panicopen in new window 结构中取出了调用 panic 时传入的 arg 参数并返回给调用方。

5.4.5 小结

panic和recover的流程如下:

  1. 编译器会负责做转换关键字的工作:
    1. 将 panic 和 recover 分别转换成 runtime.gopanicopen in new window 和 runtime.gorecoveropen in new window
    2. 将 defer 转换成 runtime.deferprocopen in new window 函数
    3. 在调用 defer 的函数末尾调用 runtime.deferreturnopen in new window 函数
  2. 在运行过程中遇到 runtime.gopanicopen in new window 方法时,会从 Goroutine 的链表依次取出 runtime._deferopen in new window 结构体并执行
  3. 若调用延迟执行函数时遇到了 runtime.gorecoveropen in new window 就会将 _panic.recovered 标记成 true 并返回 panic 的参数
    1. 在这次调用结束之后,runtime.gopanicopen in new window 会从 runtime._deferopen in new window 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recoveryopen in new window 函数进行恢复程序
    2. runtime.recoveryopen in new window 会根据传入的 pc 和 sp 跳转回 runtime.deferprocopen in new window
    3. 编译器自动生成的代码会发现 runtime.deferprocopen in new window 的返回值不为 0,这时会跳回 runtime.deferreturnopen in new window 并恢复到正常的执行流程
  4. 若没有遇到 runtime.gorecoveropen in new window 就会依次遍历所有的 runtime._deferopen in new window,并在最后调用 runtime.fatalpanicopen in new window 中止程序、打印 panic 的参数并返回错误码 2

Reference

  1. https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/open in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2