【震惊】什么?野生GoRoutine发生panic也能Recover?
序幕
这已经是老张这周背的第三个 P0 事故了。
在互联网深耕多年,老张一直靠着丰富的 CV (首先排除Computer Vision) 技巧、对轮子过目不忘的能力,倚仗一大堆搜刮来的 go mod 包,能够在 1 分钟内完成任意复杂度的需求。
这也为老张在公司里卷得了一席之地。没想到今天阴沟里翻船:
某个三方的 SDK 包里,居然起了一个野生的 Go Routine 。
func throwError() {
go func() {
panic("...")
}()
}
咋回事
众所周知,goroutine 不像其它语言的线程,没有父子关系。
任何一个 goroutine 一旦 panic 了没有 recover ,迎来的就是整个进程的 Crash 。
服务进程就像个备胎,被守护进程反复拉起、挂掉、拉起、挂掉....公司收入以肉眼可见的速度下跌。
老张看着眼前的三方包陷入了沉思...
...
也就在10分钟之后,老张心里暗暗有了主意。
走进Panic
要 cover 野生 goroutine 的 panic ,我们就得先了解 panic 。
第一步
当你的业务代码发生 panic 的时候,~~那么意味着你的代码执行了 panic ~~。在代码层面,其实就是runtime.gopanic(),我们把将这类 panic 称为显式 panic 。
那么隐式 panic 呢?例如空指针,类型转换异常...哦抱歉,没有隐式 panic ,所有的这些异常都是通过 golang 的内置库显式 panic 的,例如类型转换异常:

第二步
在 runtime.gopanic 中,会首先排除四种不可恢复的panic,直接抛出fatal。分别是:过年不回家的、
- 系统栈上发生 panic
- 内存分配时 panic
- 禁止抢占发生 panic
- g 锁在 m 上时发生 panic
第三步
然后就开始骚操作,尝试恢复,伪代码如下:
- 将
g._defer中的fn(defer方法),取出来执行。 - 如果
fn执行过程中出现了recover,那么g._panic.recovered会被改成true,同时会记下recover方法的callersp和callerpc。 - 如果没有被恢复,那就通过
_defer.link取出下一个要执行的defer。继续 2 。 - 当所有
defer都被执行完,或者g._panic.recovered为true的情况下,会搞一些inline优化,处理一些堆上的frame,然后:如果被恢复,就gogo(刚才存的sp和pc); 如果没被恢复,那就fatalpanic()。

开摆搞
卷动多年,老张从来没向困难低过头。赵鹏都能踢中卫,还有什么是不可能的。 --- 老张暗暗开始较劲
要想拦截任意 goroutine 的 panic ,没点黑科技是不行的。fatalpanic() 要开摆,我们就搞它!
搞定fatalpanic()
老张依稀想起了自己夕阳下的奔跑,那是他逝去的青春自己在淘宝开店卖 Github star 的时候,有一个顾客以2元钱一个 star 的价格买了200个 star ,这一单让老张赚到了人生的第一桶金。老张现在依然还记得这个库名字,gohook !
来看看官网的介绍:
- gohook will find go function address,then insert some NB jump code, let execute process redirect to a new good function。
- gohook 会找出 go 函数的地址,然后插入一些很屌的跳转指令,将执行流程重定向到新函数。
问题开始变得简单起来了,我们只要让代码执行我们自制的 fatalpanic() 不就好了嘛。
搞定私有方法
runtime.fatalpanic() 开头的 f,就注定了这个方法无法被我们调用,更别说获取地址了。焦灼之际,老张打开了百度:

通过 go:linkname 让编译器直接把 runtime.fatalpanic() 导出。
//go:linkname fatalpanic runtime.fatalpanic
func fatalpanic(_msgs *_panic)
搞定编译
什么?编译失败? 老张望着眼前的红字,气的发抖。又苦苦研习了一番:
-
Golang就是矫情,用都用了,还非得你引入个
unsafe包,提醒自己一下。这是高危行为,这样不可以哦。 -
因为
go build默认加会加上-complete参数,fatalpanic没有方法体会编译不通过。加个空的x.s文件,让编译器以为是cgo,可绕开这个限制。
见证奇迹的时刻

- 可以看到第25行这个看似无药可救的 panic ,被轻松拦截。
- 另一个 goroutine ,仍然正常打印时间~
附录
Q: 老张是不是作者你?
A: 每个人都是老张
Q: 为什么选择结束 goroutine ,而不是让发生错误的函数返回空?
A: 由于
runtime.gopanic()中根据callersp与callerpc恢复现场,所以自定义的 defer 植入后虽然能恢复异常,但是由于sp暂时没有办法获取,即使强改pc回跳panic发生的地方,仍然会存在函数返回时找不到方法栈的问题coredump。所以,结束 GoRoutine 看起来是一种既安全,又能满足诉求的处理方式。
Q: 如何深入了解 panic 与 recover 原理?
panic.go:958 行开始看,配合下面两篇博客食用。光速掌握panic核心。
转载自:https://juejin.cn/post/7120065204703985694