【震惊】什么?野生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