Go panic和recover函數(shù)使用細節(jié)深入探究
前情提要
關于 panic 的時機,在上篇 姿勢篇 我們已經(jīng)學習到 panic 有三種誕生方式:
- 程序猿主動:調用
panic( )函數(shù); - 編譯器的隱藏代碼:比如除零場景;
- 內核發(fā)送給進程信號:比如非法地址訪問 ;
三種都歸一到 panic( ) 函數(shù)的調用,指出 Go 的 panic 只是一個特殊的函數(shù)調用,是語言層面的處理。
知道了 panic 是怎么來的,下一步就該了解 panic 怎么去的?初學 Go 的時候,奇伢心里也常常有些疑問:
- panic 究竟是啥?是一個結構體?還是一個函數(shù)?
- 為什么 panic 會讓 Go 進程退出的 ?
- 為什么 recover 一定要放在 defer 里面才生效?
- 為什么 recover 已經(jīng)放在 defer 里面,但是進程還是沒有恢復?
- 為什么 panic 之后,還能再 panic ?有啥影響?
今天深入到代碼原理,明確以上問題。
Go 源碼版本聲明 Go 1.13.5
_panic 數(shù)據(jù)結構
看看 _panic 的數(shù)據(jù)結構:
// runtime/runtime2.go
// 關鍵結構體
type _panic struct {
argp unsafe.Pointer
arg interface{} // panic 的參數(shù)
link *_panic // 鏈接下一個 panic 結構體
recovered bool // 是否恢復,到此為止?
aborted bool // the panic was aborted
}
重點字段關注:
link字段:一個指向_panic結構體的指針,表明_panic和_defer類似,_panic可以是一個單向鏈表,就跟_defer鏈表一樣;recovered字段:重點來了,所謂的_panic是否恢復其實就是看這個字段是否為 true,recover( )其實就是修改這個字段;

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

