深度細(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)來(lái)了,所謂的?_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)體,從而直接越過(guò)了 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è)邏輯,那說(shuō)明了之前是有 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)外,說(shuō)明? _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="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;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
????????????????無(wú)?defer?延遲函數(shù)(遞歸往上),終止條件達(dá)成
????????????????
?????//?打印堆棧,退出程序
????fatalpanic
童鞋你理解了嗎?下面就來(lái)考考你哦??匆粋€(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é)束
再來(lái)一個(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,取出來(lái);
????????????????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)?來(lái)執(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 寄存器指向指令所在的地址,換句話說(shuō),就是跳轉(zhuǎn)到其他地方執(zhí)行指令去了。而不是順序執(zhí)行 gopanic 后面的指令了,補(bǔ)回來(lái)了。
_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ì)說(shuō)過(guò)。
舉個(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 流程里,則來(lái)的更直接一點(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()?并不是說(shuō)只能在 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ù)中跳出,不再回來(lái),自然就不會(huì)再?fatalpanic?;panic 為啥能嵌套?這個(gè)問題就像是在問為什么函數(shù)調(diào)用可以嵌套一樣,因?yàn)檫@個(gè)本質(zhì)是一樣的。
? ?

???
