Go panic和recover函數(shù)使用細(xì)節(jié)深入探究
前情提要
關(guān)于 panic 的時機(jī),在上篇 姿勢篇 我們已經(jīng)學(xué)習(xí)到 panic 有三種誕生方式:
- 程序猿主動:調(diào)用
panic( )
函數(shù); - 編譯器的隱藏代碼:比如除零場景;
- 內(nèi)核發(fā)送給進(jìn)程信號:比如非法地址訪問 ;
三種都?xì)w一到 panic( )
函數(shù)的調(diào)用,指出 Go 的 panic 只是一個特殊的函數(shù)調(diào)用,是語言層面的處理。
知道了 panic 是怎么來的,下一步就該了解 panic 怎么去的?初學(xué) Go 的時候,奇伢心里也常常有些疑問:
- panic 究竟是啥?是一個結(jié)構(gòu)體?還是一個函數(shù)?
- 為什么 panic 會讓 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 // 鏈接下一個 panic 結(jié)構(gòu)體 recovered bool // 是否恢復(fù),到此為止? aborted bool // the panic was aborted }
重點(diǎn)字段關(guān)注:
link
字段:一個指向_panic
結(jié)構(gòu)體的指針,表明_panic
和_defer
類似,_panic
可以是一個單向鏈表,就跟_defer
鏈表一樣;recovered
字段:重點(diǎn)來了,所謂的_panic
是否恢復(fù)其實(shí)就是看這個字段是否為 true,recover( )
其實(shí)就是修改這個字段;
再看一下 goroutine 的兩個重要字段:
type g struct { // ... _panic *_panic // panic 鏈表,這是最里的一個 _defer *_defer // defer 鏈表,這是最里的一個; // ... }
從這里我們看出:_defer
和 _panic
鏈表都是掛在 goroutine 之上的。
什么時候會導(dǎo)致 _panic
鏈表上多個元素?
panic( )
的流程下,又調(diào)用了 panic( )
函數(shù)。
這里有個細(xì)節(jié)要注意了,怎么才能做到 panic( )
流程里面再次調(diào)用 panic( )
?
劃重點(diǎn):只能是在 defer 函數(shù)上,才有可能形成一個 _panic
鏈表。因為 panic( )
函數(shù)內(nèi)只會執(zhí)行 _defer
函數(shù) !
recover 函數(shù)
為了方便講解,我們由簡單的開始分析,先看 recover 函數(shù)究竟做了什么?
defer func() { recover() }()
recover
對應(yīng)了 runtime/panic.go
中的 gorecover
函數(shù)實(shí)現(xiàn)。
gorecover 函數(shù)
func gorecover(argp uintptr) interface{} { // 只處理 gp._panic 鏈表最新的這個 _panic; gp := getg() p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
這個函數(shù)可太簡單了:
- 取出當(dāng)前 goroutine 結(jié)構(gòu)體;
- 取出當(dāng)前 goroutine 的
_panic
鏈表最新的一個_panic
,如果是非 nil 值,則進(jìn)行處理; - 該
_panic
結(jié)構(gòu)體的recovered
賦值 true,程序返回;
這就是 recover 函數(shù)的全部內(nèi)容,只給 _panic.recovered
賦值而已,不涉及代碼的神奇跳轉(zhuǎn)。而 _panic.recovered
的賦值是在 panic
函數(shù)邏輯中發(fā)揮作用。
panic 函數(shù)
panic 的實(shí)現(xiàn)在一個叫做 gopanic 的函數(shù),位于 runtime/panic.go
文件。
gopanic 函數(shù)
panic 機(jī)制最重要最重要的就是 gopanic 函數(shù)了,所有的 panic 細(xì)節(jié)盡在此。為什么 panic 會顯得晦澀,主要有兩個點(diǎn):
- 嵌套 panic 的時候,gopanic 會有遞歸執(zhí)行的場景;
- 程序指令跳轉(zhuǎn)并不是常規(guī)的函數(shù)壓棧,彈棧,在 recovery 的時候,是直接修改指令寄存器的結(jié)構(gòu)體,從而直接越過了 gopanic 后面的邏輯,甚至是多層 gopanic 遞歸的邏輯;
// runtime/panic.go func gopanic(e interface{}) { // 在棧上分配一個 _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 的時機(jī),只能跳到循環(huán)外,退出進(jìn)程了; break } // 進(jìn)到這個邏輯,那說明了之前是有 panic 了,現(xiàn)在又有 panic 發(fā)生,這里一定處于遞歸之中; if d.started { if d._panic != nil { d._panic.aborted = true } // 把這個 defer 從鏈表中摘掉; gp._defer = d.link freedefer(d) continue } // 標(biāo)記 _defer 為 started = true (panic 遞歸的時候有用) d.started = true // 記錄當(dāng)前 _defer 對應(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í)行完成,把這個 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ù)的時候 panic 函數(shù)將從此處跳出,本 gopanic 調(diào)用結(jié)束,后面的代碼永遠(yuǎn)都不會執(zhí)行。 mcall(recovery) throw("recovery failed") // mcall should not return } } // 打印錯誤信息和堆棧,并且退出進(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 鏈表,獲取到一個
_defer
延遲函數(shù); - 獲取到
_defer
延遲函數(shù),設(shè)置標(biāo)識d.started
,綁定當(dāng)前d._panic
(用以在遞歸的時候判斷); - 執(zhí)行
_defer
延遲函數(shù); - 摘掉執(zhí)行完的
_defer
函數(shù); - 判斷
_panic.recovered
是否設(shè)置為 true,進(jìn)行相應(yīng)操作;如果是 true 那么重置 pc,sp 寄存器(一般從 deferreturn 指令前開始執(zhí)行),goroutine 投遞到調(diào)度隊列,等待執(zhí)行;
- 重復(fù)以上步驟;
那些思考問題
你會發(fā)現(xiàn),更改 recovered
這個字段的時機(jī)只有在第三個步驟的時候。在任何地方,你都改不到 _panic.recovered
的值。
問題一:為什么 recover 一定要放在 defer 里面才生效?
因為,這是唯一的時機(jī) !
舉幾個淺顯對比的例子:
func main() { panic("test") recover() }
上面的例子調(diào)用了 recover( )
為什么還是 panic ?
因為根本執(zhí)行不到 recover
函數(shù),執(zhí)行順序是:
panic
gopanic
執(zhí)行 defer 鏈表
exit
有童鞋較真,那我把 recover()
放 panic("test")
前面唄?
func main() { recover() panic("test") }
不行,因為執(zhí)行 recover
的時候,還沒有 _panic
掛在 goroutine 上面呢,recover
了個寂寞。
問題二:為什么 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í)行完成,把這個 defer 從鏈表里摘掉; gp._defer = d.link
劃重點(diǎn):在 gopanic
里,只遍歷執(zhí)行當(dāng)前 goroutine 上的 _defer
函數(shù)鏈條。所以,如果掛在其他 goroutine 的 defer
函數(shù)做了 recover
,那么沒有絲毫用途。
舉一個例子:
func main() { // g1 go func() { // g2 defer func() { recover() }() }() panic("test") }
因為,panic
和 recover
在兩個不同的 goroutine,_panic
是掛在 g1 上的,recover
是在 g2 的 _defer
鏈條里。
gopanic
遍歷的是 g1 的 _defer
函數(shù)鏈表,跟 g2 八桿子打不著,g2 的 recover
自然拿不到 g1 的 _panic
結(jié)構(gòu),自然也不能設(shè)置 recovered
為 true ,所以程序還是崩了。
問題三:為什么 panic 之后,還能再 panic ?有啥影響?
這個其實(shí)很容易理解,有些童鞋可能想復(fù)雜了。gopanic
只是一個函數(shù)調(diào)用而已,那函數(shù)調(diào)用為啥不能嵌套遞歸?
當(dāng)然可以。
觸發(fā)的場景一般是:
gopanic
函數(shù)調(diào)用_defer
延遲函數(shù);defer
延遲函數(shù)里面又調(diào)用了panic/gopanic
函數(shù);
這不就有了嘛,就是個簡單的函數(shù)嵌套而已,沒啥不可以的,并且在這種場景下,_panic
結(jié)構(gòu)體就會從 gp._panic
開始形成了一個鏈表。
而 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í)行;
舉個嵌套的例子:
func main() { defer func() { // 延遲函數(shù) panic("panic again") }() panic("first") }
函數(shù)執(zhí)行:
gopanic defer 延遲函數(shù) gopanic 無 defer 延遲函數(shù)(遞歸往上),終止條件達(dá)成 // 打印堆棧,退出程序 fatalpanic
再看一個栗子:
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ù)會出打印堆棧退出進(jìn)程嗎?
答案是:不會。 猜一下輸出是啥?終端輸出結(jié)果如下:
? panic ./test_panic
=== begin ===
=== come in defer_0 ===
你猜對了嗎?奇伢給你梳理了一下完整的路線:
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 注冊時候,攜帶的指令,一般是跳轉(zhuǎn)到 deferreturn 上面幾個指令// 跳出 gopanic 的遞歸嵌套,直接到執(zhí)行 deferreturn 的地方;
defereturn
1. 執(zhí)行 defer 函數(shù)鏈,鏈條上還剩一個 defer_0,取出 defer_0;
2. 執(zhí)行 defer_0 函數(shù)
// main 函數(shù)結(jié)束
再來一個對比的例子:
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ù)會打印堆棧,并且退出嗎?
**答案是:會。**輸出如下:
? panic ./test_panic
=== begin ===
=== come in defer_0 ===
panic: panic 2goroutine 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 注冊時候,攜帶的指令,一般是跳轉(zhuǎn)到 deferreturn 上面幾個指令// 跳出 gopanic 的遞歸嵌套,執(zhí)行到 deferreturn 的地方;
defereturn1. 遍歷 defer 函數(shù)鏈,取出 defer_1
2. 摘掉 defer_1
2. 執(zhí)行 defer_1
gopanic // 第二次
1. defer 鏈表上有個 defer_0,取出來;
2. 執(zhí)行 defer_0 (defer_0 沒有做 recover,只打印了一行輸出)
3. 摘掉 defer_0,鏈表為空,跳出 for 循環(huán)
3. 執(zhí)行 fatalpanic
- exit(2) 退出進(jìn)程
你猜對了嗎?
recovery 函數(shù)
最后,看一下關(guān)鍵的 recovery 函數(shù)。在 gopanic
函數(shù)中,在循環(huán)執(zhí)行 defer 函數(shù)的時候,如果發(fā)現(xiàn) _panic.recovered
字段被設(shè)置成 true 的時候,調(diào)用 mcall(recovery)
來執(zhí)行所謂的恢復(fù)。
看一眼 recovery
函數(shù)的實(shí)現(xiàn),這個函數(shù)極其簡單,就是恢復(fù) pc,sp 寄存器,重新把 Goroutine 投遞到調(diào)度隊列中。
// runtime/panic.go func recovery(gp *g) { // 取出棧寄存器和程序計數(shù)器的值 sp := gp.sigcode0 pc := gp.sigcode1 // 重置 goroutine 的 pc,sp 寄存器; gp.sched.sp = sp gp.sched.pc = pc // 重新投入調(diào)度隊列 gogo(&gp.sched) }
重置了 pc,sp 寄存器代表什么意思?
pc 寄存器指向指令所在的地址,換句話說,就是跳轉(zhuǎn)到其他地方執(zhí)行指令去了。而不是順序執(zhí)行 gopanic 后面的指令了,補(bǔ)回來了。
_defer.pc
的指令行執(zhí)行代碼,這個指令是哪里?
這個要回憶一下 defer
的章節(jié),defer
注冊延遲函數(shù)的時候?qū)?yīng)一個 _defer
結(jié)構(gòu)體,在 new 這個結(jié)構(gòu)體的時候,_defer.pc
字段賦值的就是 new 函數(shù)的下一行指令。這個在 深入剖析 defer 篇 詳細(xì)說過。
舉個例子,如果是棧上分配的話,那么在 deferprocStack
,所以,mcall(recovery)
跳轉(zhuǎn)到這個位置,其實(shí)后續(xù)就走 deferreturn
的邏輯了,執(zhí)行后續(xù)的 _defer
函數(shù)鏈。
本次 panic 就到此為止,相當(dāng)于就恢復(fù)了程序的正常運(yùn)行。
當(dāng)然,如果后續(xù)在 defer 函數(shù)里面又出現(xiàn) panic ,那可能形成一個 _panic
的鏈條,但是每一個的處理還是一樣的。
劃重點(diǎn):函數(shù)的 call,ret 是最常見的指令跳轉(zhuǎn)。最本源的就是 pc 寄存器,函數(shù)壓棧,出棧的時候,修改的也是 pc 寄存器,在 recovery 流程里,則來的更直接一點(diǎn),直接改 pc ,sp。
for 循環(huán)外
走到 for 循環(huán)外,那程序 100% 要退出了。因為 fatalpanic 里面打印一些堆棧信息之后,直接調(diào)用 exit 退出進(jìn)程的。到這已經(jīng)沒有任何機(jī)會了,只能乖乖就義。
退出的調(diào)用就在 fatalpanic
里:
func fatalpanic(msgs *_panic) { // 1. 打印協(xié)程堆棧 // 2. 退出進(jìn)程 systemstack(func() { exit(2) }) *(*int)(nil) = 0 // not reached }
所以這個問題清楚了嘛:為什么 panic 會讓 Go 進(jìn)程退出的 ?
因為調(diào)用了 exit(2) 嘛。
總結(jié)
panic()
會退出進(jìn)程,是因為調(diào)用了 exit 的系統(tǒng)調(diào)用;recover()
并不是說只能在 defer 里面調(diào)用,而是只能在 defer 函數(shù)中才能生效,只有在 defer 函數(shù)里面,才有可能遇到_panic
結(jié)構(gòu);recover()
所在的 defer 函數(shù)必須和 panic 都是掛在同一個 goroutine 上,不能跨協(xié)程,因為gopanic
只會執(zhí)行當(dāng)前 goroutine 的延遲函數(shù);- 所謂 panic 的恢復(fù),就是重置 pc 寄存器,直接跳轉(zhuǎn)程序執(zhí)行的指令,跳轉(zhuǎn)到原本 defer 函數(shù)執(zhí)行完該跳轉(zhuǎn)的位置(
deferreturn
執(zhí)行),從gopanic
函數(shù)中跳出,不再回來,自然就不會再fatalpanic
; - panic 為啥能嵌套?這個問題就像是在問為什么函數(shù)調(diào)用可以嵌套一樣,因為這個本質(zhì)是一樣的。
后記
panic 就是一個特殊的函數(shù)調(diào)用,沒啥特殊的。只所以特殊,是因為有一些特殊的指令跳轉(zhuǎn)而已。
以上就是Go 的panic和recover函數(shù)使用細(xì)節(jié)深入探究的詳細(xì)內(nèi)容,更多關(guān)于Go panic recover函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
- Go中的 panic / recover 簡介與實(shí)踐記錄
- 一文帶你掌握Golang中panic與recover的使用方法
- GoLang中panic與recover函數(shù)以及defer語句超詳細(xì)講解
- Golang中panic與recover的區(qū)別
- Golang異常處理之defer,panic,recover的使用詳解
- Golang 錯誤捕獲Panic與Recover的使用
- 小學(xué)生也能看懂的Golang異常處理recover panic
- Go中recover與panic區(qū)別詳解
- go語言的panic和recover函數(shù)用法實(shí)例
- go語言異常panic和恢復(fù)recover用法實(shí)例
- Go語言panic和recover的用法實(shí)例
相關(guān)文章
Go?web中cookie值安全securecookie庫使用原理
這篇文章主要為大家介紹了Go?web中cookie值安全securecookie庫使用及實(shí)現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Golang?int函數(shù)使用實(shí)例全面教程
這篇文章主要為大家介紹了Golang?int函數(shù)使用實(shí)例全面教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10Golang使用Zookeeper實(shí)現(xiàn)分布式鎖
分布式鎖是一種在分布式系統(tǒng)中用于控制并發(fā)訪問的機(jī)制,ZooKeeper?和?Redis?都是常用的實(shí)現(xiàn)分布式鎖的工具,本文就來使用Zookeeper實(shí)現(xiàn)分布式鎖,希望對大家有所幫助2024-02-02使用go實(shí)現(xiàn)一個超級mini的消息隊列的示例代碼
本文主要介紹了使用go實(shí)現(xiàn)一個超級mini的消息隊列的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12Go存儲基礎(chǔ)使用direct io方法實(shí)例
這篇文章主要介紹了Go存儲基礎(chǔ)之如何使用direct io方法實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12