本系列代码基于golang1.19

1. panic的底层结构

panic在runtime中的底层表示是runtime._panic结构体。

type _panic struct {
	argp      unsafe.Pointer // 指向defer调用时参数的指针
	arg       any            // panic参数
	link      *_panic        // 连接到更早的_panic
	pc        uintptr        // 程序计数器
	sp        unsafe.Pointer // 栈指针
	recovered bool           // 当前panic是否被recover恢复
	aborted   bool           // 当前panic是否被中止
	goexit    bool           // 是否调用了runtime.Goexit
}

类似于_defer,panic也被组织成链表结构,多个panic通过link字段连接成一个链表。

_panic结构体中,pc、sp、goexit三个字段是为了修复runtime.Goexit带来的问题引入的[1].

2. 调用panic

在函数中调用panic时,底层会调用runtime.gopanic,其源码如下:

func gopanic(e any) {
	gp := getg()         // 获取当前g

    // ...
    // 此处省略部分代码

	var p _panic
	p.arg = e          // panic参数
	p.link = gp._panic // 头插
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	// 省略defer调用部分

	// ran out of deferred calls - old-school panic now
	// Because it is unsafe to call arbitrary user code after freezing
	// the world, we call preprintpanics to invoke all necessary Error
	// and String methods to prepare the panic strings before startpanic.
	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}

先看panic主干流程,首先获取当前发生了panic的g,然后新建了一个_panic,将其字段赋值后,以头插的形式插入到g_panic链表中,在函数的最后,调用了runtime.fatalpanic,这个函数实现了无法被恢复的程序崩溃:

func fatalpanic(msgs *_panic) {
	pc := getcallerpc()
	sp := getcallersp()
	gp := getg()
	var docrash bool
	// Switch to the system stack to avoid any stack growth, which
	// may make things worse if the runtime is in a bad state.

    // 切换到系统栈以避免用户栈增长
	systemstack(func() {
        // startpanic_m在应该打印panic信息时返回true
		if startpanic_m() && msgs != nil { // 
			atomic.Xadd(&runningPanicDefers, -1)

			printpanics(msgs) // 打印panic信息
		}

		docrash = dopanic_m(gp, pc, sp)
	})

	if docrash {
		crash()
	}

	systemstack(func() {
		exit(2)
	})

	*(*int)(nil) = 0 // not reached
}

runtime.fatalpanic最后调用exit(2)终止程序,返回值为2.

3. 在有defer调用时panic

上面介绍的情况是在函数运行时没有设置defer调用,然后直接panic,现在来看具有defer调用的函数发生panic时会怎样。

回顾runtime篇三:defer我们知道,程序的defer调用以_defer链表的形式存储在g中。

先大致看下源码:

func gopanic(e any) {
	gp := getg()         // 获取当前g

    // 省略部分代码

	var p _panic
	p.arg = e          // panic参数
	p.link = gp._panic // 头插
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 将当前这个panic赋值给当前defer

	atomic.Xadd(&runningPanicDefers, 1)

	// By calculating getcallerpc/getcallersp here, we avoid scanning the
	// gopanic frame (stack scanning is slow...)
	addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp())) // 这里添加了一个open-code defer

    // 检查g中是否还存在defer调用
	for {
		d := gp._defer // 尝试获取_defer
		if d == nil { // 如果没有设置_defer,则直接跳出
			break
		}

        // 如果defer被更早的panic或Goexit启动了(或者在程序到达这里之前,又触发了一个新的panic),
		// 则将当前defer移出defer链表,先前的panic将不再执行,但确保先前的Goexit继续执行

		if d.started { // defer已经被启动了
			if d._panic != nil { // defer函数中也存在panic
				d._panic.aborted = true // 终止defer的panic
			}
			d._panic = nil
			if !d.openDefer { // 未使用开放编码
				d.fn = nil
				gp._defer = d.link // 继续检查下一个defer
				freedefer(d)
				continue
			}
		}
		// Mark defer as started, but keep on list, so that traceback
		// can find and update the defer's argument frame if stack growth
		// or a garbage collection happens before executing d.fn.
		d.started = true // 将defer标记为启动

		// Record the panic that is running the defer.
		// If there is a new panic during the deferred call, that panic
		// will find d in the list and will mark d._panic (this panic) aborted.
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 将当前panic赋值給defer

		done := true
		if d.openDefer {
			done = runOpenDeferFrame(gp, d)
			if done && !d._panic.recovered {
				addOneOpenDeferFrame(gp, 0, nil)
			}
		} else {
			p.argp = unsafe.Pointer(getargp())
			d.fn() // 调用defer函数
		}
		p.argp = nil

		// Deferred function did not panic. Remove d.
		if gp._defer != d {
			throw("bad defer entry in panic")
		}
		d._panic = nil

		// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
		//GC()

		pc := d.pc
		sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
		if done { // 如果完成了defer函数
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
		}
		if p.recovered { // 如果panic被recover,则继续执行下一个panic
			// 省略recover部分
		}
	}

	preprintpanics(gp._panic)

	fatalpanic(gp._panic) // should not return
	*(*int)(nil) = 0      // not reached
}

有点复杂,结合具体程序来看这段代码:

1 package main
2
3 func main() {
4     defer func() {
5         panic("2")
6     }()
7     panic("1")
8 }

对于这个程序,我们来分析它的运行过程,首先程序在运行到第4行时,会将这个defer放入g_defer链表中,这个defer的fn字段指向func(){panic("2")}。然后程序继续执行,来到第7行,在这里调用了runtime.gopanic函数。

  1. 新建了一个_panic结构体,并将其插入到g_panic链表头部,这里称为panic1
  2. g上又新增了一个open-coded defer,现在g上有两个defer了,第一个为我们调用defer产生的(暂称为mydefer),第二个为open-coded defer,是runtime添加的(暂称openDefer)
  3. g上还有defer时,取出第一个defer,这里为mydefer
    1. mydefer没有在运行
    2. 标记mydefer为运行状态,将panic1放入mydefer的_panic字段
    3. 检查mydefer不是open-coded defer,调用_defer.fn()

这里暂停一下,我们需要明确此时g_defer_panic的状态,在调用_defer.fn()之前,g中有两个defer,分别为mydeferopenDefer,且mydefer.link = openDefer:有一个panic,为panic1。且,mydefer._panic = panic1.

继续,这里调用的_defer.fn()func(){panic("2")},在defer函数中再一次调用了panic,注意这里进行了栈帧的切换,当前的panic变成了panic2。这次调用panic的执行过程为:

  1. 新建一个新建了一个_panic结构体,并将其插入到g_panic链表头部,这里称为panic2
  2. 这回不再增加新的open-coded defer
  3. g上还有defer时,取出第一个defer,这里为mydefer
    1. mydefer在运行
      1. mydefer._panic不为空,将其标记为aborted,即把panic1标记为aborted
      2. mydefer不是open-coded defer,将mydefer.fn设为空,将mydeferg._defer链表中取出
      3. 重新检查g._defer中是否还存在defer

再次暂停,此时g上只剩下一个_deferopenDefer

继续:

  1. g上还有openDefer存在
  2. openDefer不在运行,将其标记为运行,将panic2赋值到openDefer._panic
  3. 执行openDefer
  4. 完成openDefer后,free it
  5. 检查是否有recover调用
  6. 调用fatalpanic使程序崩溃

分析完毕。

4. recover

编译器在将关键字recover转换成runtime.gorecover:

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

这个函数很简单,先是获取g,然后再获取g._panic的第一个元素,然后将其recovered标志设为true。

让我们先结合具体程序来简单看下recover流程:

1  package main
2 
3  func main() {
4      defer func() {
5          if r := recover(); r != nil {
6              println(r)
7          }
8      }()
9      panic("1")
10 }

首先程序会执行到第9行,然后一个panic1将会被添加到g._panic链表上;然后在runtime.gopanic中会添加一个openDefer, 然后调用defer.fn,会执行到recover,根据runtime.gorecover,会将g._panic的第一个元素取出,然后将其设置为可recover。

现在,g中有了两个_defermydefer.link = openDefer),一个_panicpanic1),且mydefer被设置recovered = true。我们可以开始分析recover是怎么执行的了:

而对recover的处理,还要来看runtime.gopanic:

1 func gopanic(e any) {
2     ...
3     for {
4         d := gp._defer // panic退出程序前,要执行defer
5         if d == nil {
6             break
7         }
8 
9         ...
10
11        if p.recovered { // 如果panic被recover,则继续执行下一个panic
12            gp._panic = p.link
13            if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
14                // A normal recover would bypass/abort the Goexit.  Instead,
15                // we return to the processing loop of the Goexit.
16                gp.sigcode0 = uintptr(gp._panic.sp)
17                gp.sigcode1 = uintptr(gp._panic.pc)
18                mcall(recovery)
19                throw("bypassed recovery failed") // mcall should not return
20            }
21            atomic.Xadd(&runningPanicDefers, -1)
22
23            // After a recover, remove any remaining non-started,
24            // open-coded defer entries, since the corresponding defers
25            // will be executed normally (inline). Any such entry will
26            // become stale once we run the corresponding defers inline
27            // and exit the associated stack frame. We only remove up to
28            // the first started (in-progress) open defer entry, not
29            // including the current frame, since any higher entries will
30            // be from a higher panic in progress, and will still be
31            // needed.
32            d := gp._defer
33            var prev *_defer
34            if !done {
35                // Skip our current frame, if not done. It is
36                // needed to complete any remaining defers in
37                // deferreturn()
38                prev = d
39                d = d.link
40            }
41            for d != nil { // 这里去除了已经开始的open defer
42                // 暂时省略
43            }
44
45            gp._panic = p.link
46            // Aborted panics are marked but remain on the g.panic list.
47            // Remove them from the list.
48            for gp._panic != nil && gp._panic.aborted {
49                gp._panic = gp._panic.link
50            }
51            if gp._panic == nil { // must be done with signal
52                gp.sig = 0
53            }
54            // Pass information about recovering frame to recovery.
55            gp.sigcode0 = uintptr(sp)
56            gp.sigcode1 = pc
57            mcall(recovery)
58            throw("recovery failed") // mcall should not return
59        }
60    }
61
62    // ...
63}

当程序开始进行recover时,首先在13行会做一个if判断。正常recover是会绕过Goexit的,所以为了解决这个,添加了这个判断,这样就可以保证Goexit也会被recover住,这里是通过从runtime._panic中取出了程序计数器pc和栈指针sp并且调用runtime.recovery函数触发goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值。

对于我们的程序来说,并未调用Goexit,因此这里会跳过,然后在32~43行,由于done为true,这里d将会被赋值为mydefer,然后来到45行,将defer1g._panic链表中取出,然后将余下的被标记为aborted的_panic删除,这里没有。

55、56两行设置gsigcode0sigcode1指针,用于跳转,然后57行mcall(recovery)

mcall是一个汇编实现的函数,其函数原型为:func mcall(fn func(*g)),其主要功能是切换到g0的栈,然后调用fn(g)fn(g)将不会返回,并且触发g的重新调度。

这里的fn就是recovery,来看:

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)
}

没什么特别的魔法,就是重新设置了g的一些指针,然后对其重新进行调度。

这样就完成了panic的恢复。

5. 场景分析

Q1 为什么recover必须放在defer里面 A1 不放到defer里面,没机会运行啊。。。

Q2 为什么如下使用方法不会恢复:

func main() {
	defer recover()
	panic("1")
}

A2 在调用recover()函数时,会有如下if条件:

if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
	p.recovered = true
	return p.arg
}

这里p != nil && !p.goexit && !p.recovered会满足,而argpuintptr(p.argp)并不相等,argpruntime.gopinic报告的参数指针,p.argp是最顶层 defer 函数调用的参数指针,二者并不相等。

Q3 下面这段代码将输出什么?为什么?

func main() {
	defer func() { // topdefer
		fmt.Println(recover())
	}()

	defer panic(3) // defer3
	defer panic(2) // defer2
	defer panic(1) // defer1
	panic(0)
}

A3 将输出3.

分析:在runtime.gopanic中,有如下代码:

if d.started {
	if d._panic != nil {
		d._panic.aborted = true
	}
	d._panic = nil
	if !d.openDefer {
		d.fn = nil
		gp._defer = d.link
		freedefer(d)
		continue
	}
}

当我们执行到panic(0)后,将返回执行defer1,这时defer1被设置为startedpanic(0)被设置为aborted,然后defer1被释放; 紧接着执行defer2defer2被设置为startedpanic(1)被设置为aborted,然后defer2被释放; 紧接着执行defer3defer3被设置为startedpanic(2)被设置为aborted,然后defer3被释放; 最后执行到topdefer,又因如下代码:

for gp._panic != nil && gp._panic.aborted {
	gp._panic = gp._panic.link
}

被标记为aborted的panic将被忽略,因此只剩下了panic(3)

这样,最后输出的值就是3。

Q4 为什么recover不能捕获不同goroutine的panic A4 查看runtime.gorecover源码:

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

这个函数获取了当前的g,并为其第一个_panic设置recover,跟其他g没有关系

Q5 为什么子goroutine的panic不被recover会造成整个程序的崩溃 A5 查看runtime.fatalpanic:

func fatalpanic(msgs *_panic) {
	// ...

	systemstack(func() {
		exit(2)
	})

	*(*int)(nil) = 0 // not reached
}

其在执行exit(2)时,是在systemstack上执行的,因此整个程序都会退出。

END

References

https://gfw.go101.org/article/panic-and-recover-more.html

https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/panic/

https://www.purewhite.io/2019/11/28/runtime-hacking-translate/

https://zhuanlan.zhihu.com/p/346514343

https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-panic-recover/

https://xiaomi-info.github.io/2020/01/20/go-trample-panic-recover/