深度細(xì)節(jié) | Go 的 panic 秘密都在這


前情提要

關(guān)于 panic 的時(shí)機(jī),在上篇 深度細(xì)節(jié) | Go 的 panic 的三種誕生方式 對(duì) panic 總結(jié)三種誕生方式:
程序猿主動(dòng):調(diào)用 panic( )函數(shù);編譯器的隱藏代碼:比如除零場(chǎng)景; 內(nèi)核發(fā)送給進(jìn)程信號(hào):比如非法地址訪問 ;
三種都?xì)w一到 panic( ) 函數(shù)的調(diào)用,指出 Go 的 panic 只是一個(gè)特殊的函數(shù)調(diào)用,是語(yǔ)言層面的處理。初學(xué) Go 的時(shí)候,奇伢心里也常常有些疑問:
panic 究竟是啥?是一個(gè)結(jié)構(gòu)體?還是一個(gè)函數(shù)? 為什么 panic 會(huì)讓 Go 進(jìn)程退出的 ? 為什么 recover 一定要放在 defer 里面才生效? 為什么 recover 已經(jīng)放在 defer 里面,但是進(jìn)程還是沒有恢復(fù)? 為什么 panic 之后,還能再 panic ?有啥影響?
今天便是深入到代碼原理,明確解答以上問題。Go 源碼版本聲明
Go 1.13.5

_panic 數(shù)據(jù)結(jié)構(gòu)

看看 _panic 的數(shù)據(jù)結(jié)構(gòu):
// runtime/runtime2.go
// 關(guān)鍵結(jié)構(gòu)體
type _panic struct {
argp unsafe.Pointer
arg interface{} // panic 的參數(shù)
link *_panic // 鏈接下一個(gè) panic 結(jié)構(gòu)體
recovered bool // 是否恢復(fù),到此為止?
aborted bool // the panic was aborted
}
重點(diǎn)字段關(guān)注:
link字段:一個(gè)指向_panic結(jié)構(gòu)體的指針,表明_panic和_defer類似,_panic可以是一個(gè)單向鏈表,就跟_defer鏈表一樣;recovered字段:重點(diǎn)來了,所謂的_panic是否恢復(fù)其實(shí)就是看這個(gè)字段是否為 true,recover( )其實(shí)就是修改這個(gè)字段;

再看一下 goroutine 的兩個(gè)重要字段:
type g struct {
// ...
_panic *_panic // panic 鏈表,這是最里的一個(gè)
_defer *_defer // defer 鏈表,這是最里的一個(gè);
// ...
}
從這里我們看出:_defer 和 _panic 鏈表都是掛在 goroutine 之上的。什么時(shí)候會(huì)導(dǎo)致 _panic 鏈表上多個(gè)元素?
panic( ) 的流程下,又調(diào)用了 panic( ) 函數(shù)。
這里有個(gè)細(xì)節(jié)要注意了,怎么才能做到 panic( ) 流程里面再次調(diào)用 panic( ) ?
劃重點(diǎn):只能是在 defer 函數(shù)上,才有可能形成一個(gè) _panic 鏈表。因?yàn)?panic( ) 函數(shù)內(nèi)只會(huì)執(zhí)行 _defer 函數(shù) !

recover 函數(shù)

為了方便講解,我們由簡(jiǎn)單的開始分析,先看 recover 函數(shù)究竟做了什么?
defer func() {
recover()
}()
recover 對(duì)應(yīng)了 runtime/panic.go 中的 gorecover 函數(shù)實(shí)現(xiàn)。
func gorecover(argp uintptr) interface{} {
// 只處理 gp._panic 鏈表最新的這個(gè) _panic;
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
// 只處理 gp._panic 鏈表最新的這個(gè) _panic;
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
這個(gè)函數(shù)可太簡(jiǎn)單了:
取出當(dāng)前 goroutine 結(jié)構(gòu)體; 取出當(dāng)前 goroutine 的 _panic鏈表最新的一個(gè)_panic,如果是非 nil 值,則進(jìn)行處理;該 _panic結(jié)構(gòu)體的recovered賦值 true,程序返回;
這就是 recover 函數(shù)的全部?jī)?nèi)容,只給 _panic.recovered 賦值而已,不涉及代碼的神奇跳轉(zhuǎn)。而 _panic.recovered 的賦值是在 panic 函數(shù)邏輯中發(fā)揮作用。

panic 函數(shù)

panic 的實(shí)現(xiàn)在一個(gè)叫做 gopanic 的函數(shù),位于 runtime/panic.go 文件。panic 機(jī)制最重要最重要的就是 gopanic 函數(shù)了,所有的 panic 細(xì)節(jié)盡在此。為什么 panic 會(huì)顯得晦澀,主要有兩個(gè)點(diǎn):
嵌套 panic 的時(shí)候,gopanic 會(huì)有遞歸執(zhí)行的場(chǎng)景; 程序指令跳轉(zhuǎn)并不是常規(guī)的函數(shù)壓棧,彈棧,在 recovery 的時(shí)候,是直接修改指令寄存器的結(jié)構(gòu)體,從而直接越過了 gopanic 后面的邏輯,甚至是多層 gopanic 遞歸的邏輯;
// runtime/panic.go
func gopanic(e interface{}) {
// 在棧上分配一個(gè) _panic 結(jié)構(gòu)體
var p _panic
// 把當(dāng)前最新的 _panic 掛到鏈表最前面
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
// 取出當(dāng)前最近的 defer 函數(shù);
d := gp._defer
if d == nil {
// 如果沒有 defer ,那就沒有 recover 的時(shí)機(jī),只能跳到循環(huán)外,退出進(jìn)程了;
break
}
// 進(jìn)到這個(gè)邏輯,那說明了之前是有 panic 了,現(xiàn)在又有 panic 發(fā)生,這里一定處于遞歸之中;
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
// 把這個(gè) defer 從鏈表中摘掉;
gp._defer = d.link
freedefer(d)
continue
}
// 標(biāo)記 _defer 為 started = true (panic 遞歸的時(shí)候有用)
d.started = true
// 記錄當(dāng)前 _defer 對(duì)應(yīng)的 panic
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
// 執(zhí)行 defer 函數(shù)
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
// defer 執(zhí)行完成,把這個(gè) defer 從鏈表里摘掉;
gp._defer = d.link
// 取出 pc,sp 寄存器的值;
pc := d.pc
sp := unsafe.Pointer(d.sp)
// 如果 _panic 被設(shè)置成恢復(fù),那么到此為止;
if p.recovered {
// 摘掉當(dāng)前的 _panic
gp._panic = p.link
// 如果前面還有 panic,并且是標(biāo)記了 aborted 的,那么也摘掉;
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
// panic 的流程到此為止,恢復(fù)到業(yè)務(wù)函數(shù)堆棧上執(zhí)行代碼;
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 注意:恢復(fù)的時(shí)候 panic 函數(shù)將從此處跳出,本 gopanic 調(diào)用結(jié)束,后面的代碼永遠(yuǎn)都不會(huì)執(zhí)行。
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// 打印錯(cuò)誤信息和堆棧,并且退出進(jìn)程;
preprintpanics(gp._panic)
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
上面邏輯可以拆分為循環(huán)內(nèi)和循環(huán)外兩部分去理解:
循環(huán)內(nèi):程序執(zhí)行 defer,是否恢復(fù)正常的指令執(zhí)行,一切都在循環(huán)內(nèi)決定; 循環(huán)外:一旦走到循環(huán)外,說明 _panic沒人處理,認(rèn)命吧,程序即將退出;

for 循環(huán)內(nèi)

循環(huán)內(nèi)的事情拆解成:
遍歷 goroutine 的 defer 鏈表,獲取到一個(gè) _defer延遲函數(shù);獲取到 _defer延遲函數(shù),設(shè)置標(biāo)識(shí)d.started,綁定當(dāng)前d._panic(用以在遞歸的時(shí)候判斷);執(zhí)行 _defer延遲函數(shù);摘掉執(zhí)行完的 _defer函數(shù);判斷 _panic.recovered是否設(shè)置為 true,進(jìn)行相應(yīng)操作;如果是 true 那么重置 pc,sp 寄存器(一般從 deferreturn 指令前開始執(zhí)行),goroutine 投遞到調(diào)度隊(duì)列,等待執(zhí)行; 重復(fù)以上步驟;
你會(huì)發(fā)現(xiàn),更改 recovered 這個(gè)字段的時(shí)機(jī)只有在第三個(gè)步驟的時(shí)候。在任何地方,你都改不到 _panic.recovered 的值。
問題一:為什么 recover 一定要放在 defer 里面才生效?
因?yàn)?,這是唯一的修改 _panic.recovered 字段的時(shí)機(jī) !
舉幾個(gè)對(duì)比的栗子:
func main() {
panic("test")
recover()
}
上面的例子調(diào)用了 recover( ) 為什么還是 panic ?
因?yàn)?strong>根本執(zhí)行不到 recover 函數(shù),執(zhí)行順序是:
panic
gopanic
執(zhí)行 defer 鏈表
exit
有童鞋較真,那我把 recover() 放 panic("test") 前面唄?
func main() {
recover()
panic("test")
}
不行,因?yàn)閳?zhí)行 recover 的時(shí)候,還沒有 _panic 掛在 goroutine 上面呢,recover 了個(gè)寂寞。
問題二:為什么 recover 已經(jīng)放在 defer 里面,但是進(jìn)程還是沒有恢復(fù)?
回憶一下上面 for 循環(huán)的操作:
// 步驟:遍歷 _defer 鏈表
d := gp._defer
// 步驟:執(zhí)行 defer 函數(shù)
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
// 步驟:執(zhí)行完成,把這個(gè) defer 從鏈表里摘掉;
gp._defer = d.link
劃重點(diǎn):在 gopanic 里,只遍歷執(zhí)行當(dāng)前 goroutine 上的 _defer 函數(shù)鏈條。所以,如果掛在其他 goroutine 的 defer 函數(shù)做了 recover ,那么沒有絲毫用途。
舉一個(gè)栗子:
func main() { // g1
go func() { // g2
defer func() {
recover()
}()
}()
panic("test")
}
因?yàn)椋?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">panic 和 recover 在兩個(gè)不同的 goroutine,_panic 是掛在 g1 上的,recover 是在 g2 的 _defer 鏈條里。
gopanic 遍歷的是 g1 的 _defer 函數(shù)鏈表,跟 g2 八桿子打不著,g2 的 recover 自然拿不到 g1 的 _panic 結(jié)構(gòu),自然也不能設(shè)置 recovered 為 true ,所以程序還是崩了。
問題三:為什么 panic 之后,還能再 panic ?有啥影響?
這個(gè)其實(shí)很容易理解,有些童鞋可能想復(fù)雜了。gopanic 只是一個(gè)函數(shù)調(diào)用而已,那函數(shù)調(diào)用為啥不能嵌套遞歸?
當(dāng)然可以。
觸發(fā)的場(chǎng)景一般是:
gopanic函數(shù)調(diào)用_defer延遲函數(shù);defer延遲函數(shù)里面又調(diào)用了panic/gopanic函數(shù);
這不就有了嘛,就是個(gè)簡(jiǎn)單的函數(shù)嵌套而已,沒啥不可以的,并且在這種場(chǎng)景下,_panic 結(jié)構(gòu)體就會(huì)從 gp._panic 開始形成了一個(gè)鏈表。
而 gopanic 函數(shù)指令執(zhí)行的特殊在于兩點(diǎn):
_panic被人設(shè)置成 recovered 之后,重置 pc,sp 寄存器,直接跨越 gopanic (還有嵌套的函數(shù)棧),跳轉(zhuǎn)到正常業(yè)務(wù)流程中;循環(huán)之外,等到最后,沒人處理 _panic數(shù)據(jù),那就 exit 退出進(jìn)程,終止后續(xù)所有指令的執(zhí)行;
舉個(gè)嵌套的栗子:
func main() {
defer func() { // 延遲函數(shù)
panic("panic again")
}()
panic("first")
}
函數(shù)執(zhí)行:
gopanic
defer 延遲函數(shù)
gopanic
無 defer 延遲函數(shù)(遞歸往上),終止條件達(dá)成
// 打印堆棧,退出程序
fatalpanic
童鞋你理解了嗎?下面就來考考你哦??匆粋€(gè)栗子:
func main() {
println("=== begin ===")
defer func() { // defer_0
println("=== come in defer_0 ===")
}()
defer func() { // defer_1
recover()
}()
defer func() { // defer_2
panic("panic 2")
}()
panic("panic 1")
println("=== end ===")
}
上面的函數(shù)會(huì)出打印堆棧退出進(jìn)程嗎?
答案是:不會(huì)。 猜一下輸出是啥?終端輸出結(jié)果如下:
? panic ./test_panic
=== begin ===
=== come in defer_0 ===
你猜對(duì)了嗎?奇伢給你梳理了一下完整的路線:
main
gopanic // 第一次
1. 取出 defer_2,設(shè)置 started
2. 執(zhí)行 defer_2
gopanic // 第二次
1. 取出 defer_2,panic 設(shè)置成 aborted
2. 把 defer_2 從鏈表中摘掉
3. 執(zhí)行 defer_1
- 執(zhí)行 recover
4. 摘掉 defer_1
5. 執(zhí)行 recovery ,重置 pc 寄存器,跳轉(zhuǎn)到 defer_1 注冊(cè)時(shí)候,攜帶的指令,一般是跳轉(zhuǎn)到 deferreturn 上面幾個(gè)指令
// 跳出 gopanic 的遞歸嵌套,直接到執(zhí)行 deferreturn 的地方;
defereturn
1. 執(zhí)行 defer 函數(shù)鏈,鏈條上還剩一個(gè) defer_0,取出 defer_0;
2. 執(zhí)行 defer_0 函數(shù)
// main 函數(shù)結(jié)束
再來一個(gè)對(duì)比的例子:
func main() {
println("=== begin ===")
defer func() { // defer_0
println("=== come in defer_0 ===")
}()
defer func() { // defer_1
panic("panic 2")
}()
defer func() { // defer_2
recover()
}()
panic("panic 1")
println("=== end ===")
}
上面的函數(shù)會(huì)打印堆棧,并且退出嗎?
答案是:會(huì)。輸出如下:
? panic ./test_panic
=== begin ===
=== come in defer_0 ===
panic: panic 2
goroutine 1 [running]:
main.main.func2()
/Users/code/gopher/src/panic/test_panic.go:9 +0x39
main.main()
/Users/code/gopher/src/panic/test_panic.go:11 +0xf7
奇伢給你梳理的執(zhí)行路徑如下:
main
gopanic // 第一次
1. 取出 defer_2,設(shè)置 started
2. 執(zhí)行 defer_2
- 執(zhí)行 recover,panic_1 字段被設(shè)置 recovered
3. 把 defer_2 從鏈表中摘掉
4. 執(zhí)行 recovery ,重置 pc 寄存器,跳轉(zhuǎn)到 defer_1 注冊(cè)時(shí)候,攜帶的指令,一般是跳轉(zhuǎn)到 deferreturn 上面幾個(gè)指令
// 跳出 gopanic 的遞歸嵌套,執(zhí)行到 deferreturn 的地方;
defereturn
1. 遍歷 defer 函數(shù)鏈,取出 defer_1
2. 摘掉 defer_1
2. 執(zhí)行 defer_1
gopanic // 第二次
1. defer 鏈表上有個(gè) defer_0,取出來;
2. 執(zhí)行 defer_0 (defer_0 沒有做 recover,只打印了一行輸出)
3. 摘掉 defer_0,鏈表為空,跳出 for 循環(huán)
3. 執(zhí)行 fatalpanic
- exit(2) 退出進(jìn)程
你猜對(duì)了嗎?
最后,看一下關(guān)鍵的 recovery 函數(shù)。在 gopanic 函數(shù)中,在循環(huán)執(zhí)行 defer 函數(shù)的時(shí)候,如果發(fā)現(xiàn) _panic.recovered 字段被設(shè)置成 true 的時(shí)候,調(diào)用 mcall(recovery) 來執(zhí)行所謂的恢復(fù)。
看一眼 recovery 函數(shù)的實(shí)現(xiàn),這個(gè)函數(shù)極其簡(jiǎn)單,就是恢復(fù) pc,sp 寄存器,重新把 Goroutine 投遞到調(diào)度隊(duì)列中。
// runtime/panic.go
func recovery(gp *g) {
// 取出棧寄存器和程序計(jì)數(shù)器的值
sp := gp.sigcode0
pc := gp.sigcode1
// 重置 goroutine 的 pc,sp 寄存器;
gp.sched.sp = sp
gp.sched.pc = pc
// 重新投入調(diào)度隊(duì)列
gogo(&gp.sched)
}
重置了 pc,sp 寄存器代表什么意思?
pc 寄存器指向指令所在的地址,換句話說,就是跳轉(zhuǎn)到其他地方執(zhí)行指令去了。而不是順序執(zhí)行 gopanic 后面的指令了,補(bǔ)回來了。
_defer.pc 的指令行,這個(gè)指令是哪里?
這個(gè)要回憶一下 defer 的章節(jié),defer 注冊(cè)延遲函數(shù)的時(shí)候?qū)?yīng)一個(gè) _defer 結(jié)構(gòu)體,在 new 這個(gè)結(jié)構(gòu)體的時(shí)候,_defer.pc 字段賦值的就是 new 函數(shù)的下一行指令。這個(gè)在 Golang 最細(xì)節(jié)篇 — 解密 defer 原理,究竟背著程序猿做了多少事情? 詳細(xì)說過。
舉個(gè)例子,如果是棧上分配的話,那么在 deferprocStack ,所以,mcall(recovery) 跳轉(zhuǎn)到這個(gè)位置,其實(shí)后續(xù)就走 deferreturn 的邏輯了,執(zhí)行后續(xù)的 _defer 函數(shù)鏈。
本次 panic 就到此為止,相當(dāng)于就恢復(fù)了程序的正常運(yùn)行。
當(dāng)然,如果后續(xù)在 defer 函數(shù)里面又出現(xiàn) panic ,那可能形成一個(gè) _panic 的鏈條,但是每一個(gè)的處理還是一樣的。
劃重點(diǎn):函數(shù)的 call,ret 是最常見的指令跳轉(zhuǎn)。最本源的就是 pc 寄存器,函數(shù)壓棧,出棧的時(shí)候,修改的也是 pc 寄存器,在 recovery 流程里,則來的更直接一點(diǎn),直接改 pc ,sp。

for 循環(huán)外

走到 for 循環(huán)外,那程序 100% 要退出了。因?yàn)?fatalpanic 里面打印一些堆棧信息之后,直接調(diào)用 exit 退出進(jìn)程的。到這已經(jīng)沒有任何機(jī)會(huì)了,只能乖乖退出進(jìn)程。
退出的調(diào)用就在 fatalpanic 里:
func fatalpanic(msgs *_panic) {
// 1. 打印協(xié)程堆棧
// 2. 退出進(jìn)程
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}
所以這個(gè)問題清楚了嘛:為什么 panic 會(huì)讓 Go 進(jìn)程退出的 ?
還能為啥,因?yàn)檎{(diào)用了 exit(2) 嘛。

總結(jié)

panic()會(huì)退出進(jìn)程,是因?yàn)檎{(diào)用了 exit 的系統(tǒng)調(diào)用;recover()并不是說只能在 defer 里面調(diào)用,而是只能在 defer 函數(shù)中才能生效,只有在 defer 函數(shù)里面,才有可能遇到_panic結(jié)構(gòu);recover()所在的 defer 函數(shù)必須和 panic 都是掛在同一個(gè) goroutine 上,不能跨協(xié)程,因?yàn)?gopanic只會(huì)執(zhí)行當(dāng)前 goroutine 的延遲函數(shù);panic 的恢復(fù),就是重置 pc 寄存器,直接跳轉(zhuǎn)程序執(zhí)行的指令,跳轉(zhuǎn)到原本 defer 函數(shù)執(zhí)行完該跳轉(zhuǎn)的位置( deferreturn執(zhí)行),從gopanic函數(shù)中跳出,不再回來,自然就不會(huì)再fatalpanic;panic 為啥能嵌套?這個(gè)問題就像是在問為什么函數(shù)調(diào)用可以嵌套一樣,因?yàn)檫@個(gè)本質(zhì)是一樣的。

后記

panic 就是一個(gè)函數(shù)調(diào)用,沒啥特殊的。點(diǎn)贊、在看 是對(duì)奇伢最大的支持。
~完~

往期推薦

往期推薦
堅(jiān)持思考,方向比努力更重要。關(guān)注我:奇伢云存儲(chǔ)
