本系列代码基于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,分别为mydefer、openDefer,且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设为空,将mydefer从g._defer链表中取出
      3. 重新检查g._defer中是否还存在defer

再次暂停,此时g上只剩下一个_defer:openDefer

继续:

  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中有了两个_defer(mydefer.link = openDefer),一个_panic(panic1),且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行,将defer1从g._panic链表中取出,然后将余下的被标记为aborted的_panic删除,这里没有。

55、56两行设置g的sigcode0、sigcode1指针,用于跳转,然后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会满足,而argp和uintptr(p.argp)并不相等,argp是runtime.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被设置为started,panic(0)被设置为aborted,然后defer1被释放; 紧接着执行defer2,defer2被设置为started,panic(1)被设置为aborted,然后defer2被释放; 紧接着执行defer3,defer3被设置为started,panic(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/