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