5.4 panic and recover

Kesa...大约 5 分钟golang

Golang 中的panicrecover

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

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

golang-panic
golang-panic

5.4.1 现象

使用panicrecover时,会有:

  • 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所在的goroutinedefer被触发。

因为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()

上述代码因为recoverpanic调用之前被调用了,不会生效。需要在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 小结

panicrecover的流程如下:

  1. 编译器会负责做转换关键字的工作:
    1. panicrecover 分别转换成 runtime.gopanicopen in new windowruntime.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 会根据传入的 pcsp 跳转回 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