Go 异常处理流程

前言

有这样一段代码:

func main() {
	// 捕捉异常
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	go func() {
		fmt.Println("start goroutine")
		panic("error")
	}()
	// 等待
	time.Sleep(time.Second * 100)
}

浅猜一下结果? 没错, 进程崩溃了, defer没有捕捉到异常.

面对这个问题, 可能有的人想的是: “哦, defer不能处理其他goroutine的异常”. 但我正巧闲得慌, 索性研究一下Go中异常是如何处理的.

探究

Go语言现在已经实现自举了, 所以其源码也是Go, 不用去看C了. 源码地址: https://github.com/golang/go. 以下源码基于分支release-branch.go1.18

因为当前主要研究的是异常的处理, 所以对协程的原理及defer的调用不做过多探究.

结构体

Go中, 每个协程都有一个结构体来记录其运行信息, 这个结构体的名字是g. 内容大致如下(定义在文件runtime.runtime2.go):

type g struct {
	// ...
  // 当前协程已出发的异常链
  _panic    *_panic
	// 记录当前协程的所有 defer 的链表
	_defer    *_defer
	// ...
}

其他的字段都跳过, 只看_defer字段, 也就是说在Godefer和异常是记录在当前协程中的. 再回到开头的问题, 新启动的协程是没有defer函数的, 自然也就无法捕捉到异常.

既然是研究异常处理, _panic这个结构体自然也得瞅瞅了.

type _panic struct {
	// defer 函数用的, 具体等到研究 defer的时候再看
	argp      unsafe.Pointer
	// 调用 panic 函数时的参数
	arg       any
	// 指向上一个 panic, 组成一个 panic 链表
	// 因为在 panic 处理时, 可能再次发生异常
	// 比如在 defer 函数中发生 panic
	link      *_panic
	// 当前异常是否已经被 recover 处理
	recovered bool
	// 是否被强制终止
	aborted   bool
	goexit    bool
	pc        uintptr
	sp        unsafe.Pointer
}

其中pc sp goexit三个字段, 在defer中发生panic, 然后上层defer对异常进行了recover时使用的, 具体作用我也没整太明白, 可看这次commit 以及这个 issuse

OK, 到这里, 我们和异常的结构体见了一面了, 但异常触发时是如何处理的呢?

处理流程

先上一张流程图来对异常的处理流程进行较为形象的表达.

未命名文件

从上图中, 可以基本看出处理的流程. 不过还是康一下源码吧(为了不影响篇幅, 只保留了部分内容).

你问我怎么知道调用了源码中的哪个函数? 使用命令go tool compile -N -l -S main.go看一下咯.

// 处理异常的函数
func gopanic(e any) {
	gp := getg()
	// ...
	// 创建新的异常并添加到协程的 panic 链表头
	var p _panic
	p.arg = e
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
	//...
	for {
		d := gp._defer
		//...
		done := true
		// 调用 defer 函数
		// 关于 openDefer, 在这里先按下不表
		if d.openDefer {
			done = runOpenDeferFrame(gp, d)
			if done && !d._panic.recovered {
				addOneOpenDeferFrame(gp, 0, nil)
			}
		} else {
			p.argp = unsafe.Pointer(getargp())
			d.fn()
		}
		//...
		if done {
			//...
			// 将当前 defer 从协程的 defer 链中去掉
			freedefer(d)
		}
		// 异常已经被处理啦
		if p.recovered {
			//...
			// 恢复协程运行. 这里恢复的 recovery 方法不返回
			mcall(recovery)
			throw("recovery failed")
		}
	}
	// 打印 panic 信息
	preprintpanics(gp._panic)
	// 进程终止
	fatalpanic(gp._panic)
	*(*int)(nil) = 0
}
// 当调用 recover 时触发
// 逻辑很简单, 就是从异常链表的头部将异常取出来
// 因为新的异常会放到 _panic 链表头, 所以这里拿到的是最新的异常
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
}

gopanic函数的异常处理流程来看, 异常在gorecover中一旦被处理, 就会将p.recovered标记为true. 而外层一旦检测到其值为true就会恢复运行. 看着貌似没什么问题哈, 但是还记不记得_panic是一个链表呀? 这不是才处理了一个异常么? 如果有其他异常不就丢了么? 比如下面这种情况:

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	defer func() {
		panic("error 2")
	}()
	panic("error")
}

没错, 我们在先调用的defer中再次抛出异常, 此时, error这个异常就没有啦. 现象是符合我们前面的分析的.

系统级异常

在运行时有一些异常是无法通过recover捕获的. 比如调用throw函数的错误, throw函数源码如下:

func throw(s string) {
	systemstack(func() {
		print("fatal error: ", s, "\n")
	})
	gp := getg()
	if gp.m.throwing == 0 {
		gp.m.throwing = 1
	}
	//  直接强制停止
	fatalthrow()
	*(*int)(nil) = 0 // not reached
}

那么哪些操作会触发无法捕获的异常呢? 在系统实现上到处有调用throw函数的, 比如:

  • map的并发读写
  • 占内存耗尽, 比如递归太深
  • go启动的函数为nil

OK, 至此, 虽然在源码层面没有分析的特别细致, 但是对异常的处理流程基本能做到心中有数啦

原文地址: https://hujingnb.com/archives/853

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值