Go 語言中 panic 和 recover 搭配使用
本次主要聊聊 Go 語言中關(guān)于 panic 和 recover 搭配使用 ,以及 panic 的基本原理
最近工作中審查代碼的時(shí)候發(fā)現(xiàn)一段代碼,類似于如下這樣,將 recover 放到一個(gè)子協(xié)程里面,期望去捕獲主協(xié)程的程序異常
圖片
看到此處,是否會(huì)想這段代碼在項(xiàng)目中是想當(dāng)然寫出來的吧,然而平日中,大多問題是出現(xiàn)在認(rèn)知偏差上,那么本次,我們就來消除一下這個(gè)認(rèn)知偏差
關(guān)于 Go 語言中顯示的使用 panic 的地方不多,一般 panic ,基本上會(huì)出現(xiàn)在咱們程序出現(xiàn)異常退出的時(shí)候
例如訪問了空指針里面的值,則會(huì) panic 報(bào)錯(cuò)無效的內(nèi)存地址,又例如訪問量數(shù)組中不存在的數(shù)組所索引,或者切片索引,那么會(huì)報(bào)錯(cuò) panic 數(shù)組越界等等
可是碰到這些 panic 的時(shí)候,實(shí)際上我們并不期望當(dāng)前的服務(wù)直接掛掉,而是期望這個(gè)異常能夠被識(shí)別,且不影響程序其他部分的模塊運(yùn)行
正常捕獲異常
在 Go 中可以將 defer 和 recover 進(jìn)行搭配使用,可以捕獲和處理大部分的異常情況,例如可以這樣
圖片
這里可以看到,recover 捕獲異常和發(fā)生異常的部分是在同一個(gè)協(xié)程中,實(shí)驗(yàn)證明是可以正常捕獲并且處理異常
并沒有捕獲到異常
- 直接不做顯示的 recover,自然 panic 程序崩潰會(huì)如期而至,此處我們顯示的使用 panic 函數(shù)來制造恐慌
func main() {
log.SetFlags(log.Lshortfile)
panic("panic coming...")
}
圖片
- 不使用 defer 來進(jìn)行處理
func main() {
log.SetFlags(log.Lshortfile)
if err := recover(); err != nil {
log.Println("recover panic : ", err)
}
panic("panic coming...")
}
圖片
自然 recover 函數(shù)是在 panic 調(diào)用之前就已經(jīng)執(zhí)行,此時(shí)是還沒有異常需要捕獲和恢復(fù)的,待程序運(yùn)行到 panic 處的時(shí)候,實(shí)際上并沒有沒有處理程序崩潰的異常
結(jié)果,仍然是程序崩潰
- 當(dāng)然,還有文章開頭提到的出現(xiàn) panic 的位置和捕獲和處理程序崩潰異常的位置不在同一個(gè)協(xié)程,自然也是沒法捕獲到的,這一點(diǎn)需要注意,其他的語言可能不是這樣,但是 Go 中是這樣的
panic 基本原理
看了上述現(xiàn)象,實(shí)際上還是對(duì)知識(shí)點(diǎn)理解得不夠,使用的時(shí)候想當(dāng)然了,就像使用 defer 一樣,如果對(duì)他不夠了解的話,使用的時(shí)候,確實(shí)會(huì)出現(xiàn)一些奇奇怪怪的現(xiàn)象,對(duì)于 defer 的使用可以查看文末的文章地址
- panic 函數(shù)和 recover 函數(shù),Go 源碼builtin\builtin.go中可以看到注釋
圖片
注釋中有說關(guān)于 panic 和 recover 的使用是作用于當(dāng)前協(xié)程的,因此我們使用的時(shí)候,如果跨協(xié)程教程使用,自然不會(huì)達(dá)到我們期望的效果
- 繼續(xù)查看關(guān)于 panic 的源碼,實(shí)際上是一個(gè)結(jié)構(gòu),放到 defer 結(jié)構(gòu)里面的一個(gè)指針,源碼位置:runtime\runtime2.go
圖片
_panic 的結(jié)構(gòu)如下:
type _panic struct {
argp unsafe.Pointer
arg interface{}
link *_panic
pc uintptr
sp unsafe.Pointer
recovered bool
aborted bool
goexit bool
}
上述兩個(gè)結(jié)構(gòu)表達(dá)的意思是,程序中出現(xiàn) panic 的時(shí)候,實(shí)際上都會(huì)創(chuàng)建一個(gè) _panic 結(jié)構(gòu),這個(gè) _panic 結(jié)構(gòu)里面存儲(chǔ)了當(dāng)前程序崩潰的一些必要信息,如下:
- argp
是一個(gè) unsafe.Pointer 類型的成員,指向 defer 調(diào)用參數(shù)的指針
- arg
出現(xiàn) panic 的原因,如果我們顯示調(diào)用 panic,那么就是我們填入 panic 函數(shù)中的參數(shù),例如上述的 panic coming ...
- link
是一個(gè)指針,指向上一個(gè),最近的一個(gè) _panic 結(jié)構(gòu)的地址,實(shí)際上此處就可以看到這個(gè)指針對(duì)應(yīng)的是一個(gè)鏈表,一個(gè)又多個(gè) _panic 結(jié)構(gòu)組成的鏈表
圖片
- recovered
panic 是否已經(jīng)處理完畢,即當(dāng)前的這個(gè) panic 是否是已經(jīng)被 recover 了
- aborted
表示當(dāng)前的 panic 是否被中止
- 對(duì)于 pc 和 sp 自然就是我們熟知的 pc 通用寄存器,在匯編中是指向當(dāng)前運(yùn)行指令的下一條指令,sp 則是棧指針 stack pointer,用于入棧和出棧的
我們知道運(yùn)行函數(shù)的時(shí)候需要入棧,運(yùn)行完畢之后需要出棧
源碼中的 runtime.gopanic
那么我們繼續(xù)來閱讀源碼,上述看到 sp 和 pc ,那么我們就簡(jiǎn)單寫一個(gè) panic 的代碼來看看匯編到底是怎么執(zhí)行的,不用擔(dān)心看不懂,我們只需要看關(guān)鍵詞就行
還是上面的程序
圖片
程序運(yùn)行的時(shí)候可以執(zhí)行 go tool compile -S main.go
可以看到匯編代碼,可能其他的看不懂,但是我們可以看到如下關(guān)鍵詞
圖片
- log.(*Logger).SetFlags(SB) 即是執(zhí)行到我們調(diào)用 log 去設(shè)置參數(shù)
- 程序走到 panic 函數(shù)的時(shí)候,實(shí)際上是執(zhí)行了 runtime.gopanic 函數(shù),我們一起看看源碼
圖片
代碼中可以看到 p.recovered 邏輯下的關(guān)于 recover 的邏輯被刪除掉了,在文章的后面會(huì)繼續(xù)說到,當(dāng)前我們先關(guān)注 panic 的事項(xiàng)
runtime.gopanic 程序的邏輯大體是這樣的
- 獲取當(dāng)前 協(xié)程 的指針
- 初始化一個(gè) _panic 結(jié)構(gòu) p,并將當(dāng)前協(xié)程上對(duì)應(yīng)的數(shù)據(jù)賦值給到 p 上,且將 當(dāng)前協(xié)程 _panic 掛到 link 上
- 進(jìn)入循環(huán)后,拿到當(dāng)前協(xié)程的 _defer 數(shù)據(jù)
- 查看 _defer 指針數(shù)據(jù) 中是否有 defer 調(diào)用,如果有則執(zhí)行
- 處理完基本邏輯之后,打印 panic 信息,例如我們 demo 中的 panic coming ... 信息
- 最終退出程序
Xdm 可以看上圖,自己捋一捋邏輯就清晰了
接著,我們來看
fatalpanic
圖片
通過 runtime.gopanic 我們可以看到 fatalpanic 函數(shù)基本上就是做一個(gè)收尾工作了,如果上述程序處理完畢之后, fatalpanic 校驗(yàn)到 panic 是需要 recover 的,那么就打印 [recovered]
打印的這個(gè)信息是由 上圖中 printpanics 完成的
圖片
這下知道 panic 是如何去執(zhí)行的了,那么對(duì)于現(xiàn)在來研究 recover 是如何落實(shí)的
recover
還是同一個(gè)例子,咱們將 defer 部分的代碼注打開,來繼續(xù)看看效果
func main() {
log.SetFlags(log.Lshortfile)
defer func() {
if err := recover(); err != nil {
log.Println("recover panic : ", err)
}
}()
panic("panic coming...")
}
自然效果是我們期望的,捕獲到了異常,且處理了
圖片
繼續(xù)打印匯編來查看一下關(guān)鍵詞,是否有我們期望的函數(shù)出現(xiàn)
圖片
圖片
此處我們可以看到,實(shí)際 Go 中調(diào)用了多個(gè)函數(shù)
- runtime.gorecover
- main.main.opendefer
- log.(*Logger).SetFlags
- runtime.gopanic
- runtime.deferreturn
自然明眼人都看的出現(xiàn),關(guān)鍵的函數(shù)實(shí)現(xiàn)自然是 runtime.gorecover ,那么我們來一探究竟
runtime.gorecover
圖片
查看源碼我們可以知道, runtime.gorecover 實(shí)際上就是根據(jù)當(dāng)前協(xié)程的 _panic 結(jié)構(gòu)數(shù)據(jù)來判斷是否需要恢復(fù),如果需要?jiǎng)t將 p.recovered = true
自然在這里將當(dāng)前協(xié)程的數(shù)據(jù)修改掉,正是為了后續(xù)執(zhí)行 runtime.gopanic 的時(shí)候提供保障, runtime.gopanic 執(zhí)行的時(shí)候就會(huì)去判斷和處理這個(gè) p.recovered
前文中提到的關(guān)于 runtime.gopanic 中 處理 p.recovered 的邏輯是這樣的
圖片
圖片
- 如上可以看到 runtime.gorecover 去對(duì) p.recovered 設(shè)置是否恢復(fù)
- runtime.gopanic 中校驗(yàn) p.recovered 已處理,則執(zhí)行 recovery 函數(shù)
- recovery 函數(shù)中去處理對(duì)應(yīng)的寄存器的值去維護(hù)上下文
- 最后我們可以看到最終調(diào)用 gogo 函數(shù)跳回原來調(diào)用的位置
因此,當(dāng)我們?cè)谕粋€(gè)協(xié)程中出現(xiàn)了 panic,且在同一個(gè)協(xié)程中去使用 defer 來配合 recover 來進(jìn)行捕獲異常和處理異常,就可以得以實(shí)現(xiàn),看到這里,有沒有覺得還是蠻簡(jiǎn)單的,不就是去對(duì)一個(gè) p.recovered 進(jìn)行配合處理嗎
自然,表面上是這樣,其中對(duì)于寄存器的各種數(shù)據(jù)處理涉及的內(nèi)容還是不少的,不過這不在我們今天聊的范疇中了
總結(jié)
至此,相信你已經(jīng)知道了這些
- 為什么 panic 和 defer ,recover 配合使用的時(shí)候要在同一個(gè)協(xié)程中了吧
- 相信你還知道了 panic 和 recover 的處理流程