本次主要聊聊 Go 語言中關于 panic 和 recover 搭配使用 ,以及 panic 的基本原理
最近工作中審查代碼的時候發現一段代碼,類似于如下這樣,將 recover 放到一個子協程里面,期望去捕獲主協程的程序異常
圖片
看到此處,是否會想這段代碼在項目中是想當然寫出來的吧,然而平日中,大多問題是出現在認知偏差上,那么本次,我們就來消除一下這個認知偏差
關于 Go 語言中顯示的使用 panic 的地方不多,一般 panic ,基本上會出現在咱們程序出現異常退出的時候
例如訪問了空指針里面的值,則會 panic 報錯無效的內存地址,又例如訪問量數組中不存在的數組所索引,或者切片索引,那么會報錯 panic 數組越界等等
可是碰到這些 panic 的時候,實際上我們并不期望當前的服務直接掛掉,而是期望這個異常能夠被識別,且不影響程序其他部分的模塊運行
在 Go 中可以將 defer 和 recover 進行搭配使用,可以捕獲和處理大部分的異常情況,例如可以這樣
圖片
這里可以看到,recover 捕獲異常和發生異常的部分是在同一個協程中,實驗證明是可以正常捕獲并且處理異常
func main() { log.SetFlags(log.Lshortfile) panic("panic coming...")}
圖片
func main() { log.SetFlags(log.Lshortfile) if err := recover(); err != nil { log.Println("recover panic : ", err) } panic("panic coming...")}
圖片
自然 recover 函數是在 panic 調用之前就已經執行,此時是還沒有異常需要捕獲和恢復的,待程序運行到 panic 處的時候,實際上并沒有沒有處理程序崩潰的異常
結果,仍然是程序崩潰
看了上述現象,實際上還是對知識點理解得不夠,使用的時候想當然了,就像使用 defer 一樣,如果對他不夠了解的話,使用的時候,確實會出現一些奇奇怪怪的現象,對于 defer 的使用可以查看文末的文章地址
圖片
注釋中有說關于 panic 和 recover 的使用是作用于當前協程的,因此我們使用的時候,如果跨協程教程使用,自然不會達到我們期望的效果
圖片
_panic 的結構如下:
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic pc uintptr sp unsafe.Pointer recovered bool aborted bool goexit bool}
上述兩個結構表達的意思是,程序中出現 panic 的時候,實際上都會創建一個 _panic 結構,這個 _panic 結構里面存儲了當前程序崩潰的一些必要信息,如下:
是一個 unsafe.Pointer 類型的成員,指向 defer 調用參數的指針
出現 panic 的原因,如果我們顯示調用 panic,那么就是我們填入 panic 函數中的參數,例如上述的 panic coming ...
是一個指針,指向上一個,最近的一個 _panic 結構的地址,實際上此處就可以看到這個指針對應的是一個鏈表,一個又多個 _panic 結構組成的鏈表
圖片
panic 是否已經處理完畢,即當前的這個 panic 是否是已經被 recover 了
表示當前的 panic 是否被中止
我們知道運行函數的時候需要入棧,運行完畢之后需要出棧
那么我們繼續來閱讀源碼,上述看到 sp 和 pc ,那么我們就簡單寫一個 panic 的代碼來看看匯編到底是怎么執行的,不用擔心看不懂,我們只需要看關鍵詞就行
還是上面的程序
圖片
程序運行的時候可以執行 go tool compile -S main.go
可以看到匯編代碼,可能其他的看不懂,但是我們可以看到如下關鍵詞
圖片
圖片
代碼中可以看到 p.recovered 邏輯下的關于 recover 的邏輯被刪除掉了,在文章的后面會繼續說到,當前我們先關注 panic 的事項
runtime.gopanic 程序的邏輯大體是這樣的
Xdm 可以看上圖,自己捋一捋邏輯就清晰了
接著,我們來看
圖片
通過 runtime.gopanic 我們可以看到 fatalpanic 函數基本上就是做一個收尾工作了,如果上述程序處理完畢之后, fatalpanic 校驗到 panic 是需要 recover 的,那么就打印 [recovered]
打印的這個信息是由 上圖中 printpanics 完成的
圖片
這下知道 panic 是如何去執行的了,那么對于現在來研究 recover 是如何落實的
還是同一個例子,咱們將 defer 部分的代碼注打開,來繼續看看效果
func main() { log.SetFlags(log.Lshortfile) defer func() { if err := recover(); err != nil { log.Println("recover panic : ", err) } }() panic("panic coming...")}
自然效果是我們期望的,捕獲到了異常,且處理了
圖片
繼續打印匯編來查看一下關鍵詞,是否有我們期望的函數出現
圖片
圖片
此處我們可以看到,實際 Go 中調用了多個函數
自然明眼人都看的出現,關鍵的函數實現自然是 runtime.gorecover ,那么我們來一探究竟
圖片
查看源碼我們可以知道, runtime.gorecover 實際上就是根據當前協程的 _panic 結構數據來判斷是否需要恢復,如果需要則將 p.recovered = true
自然在這里將當前協程的數據修改掉,正是為了后續執行 runtime.gopanic 的時候提供保障, runtime.gopanic 執行的時候就會去判斷和處理這個 p.recovered
前文中提到的關于 runtime.gopanic 中 處理 p.recovered 的邏輯是這樣的
圖片
圖片
因此,當我們在同一個協程中出現了 panic,且在同一個協程中去使用 defer 來配合 recover 來進行捕獲異常和處理異常,就可以得以實現,看到這里,有沒有覺得還是蠻簡單的,不就是去對一個 p.recovered 進行配合處理嗎
自然,表面上是這樣,其中對于寄存器的各種數據處理涉及的內容還是不少的,不過這不在我們今天聊的范疇中了
至此,相信你已經知道了這些
本文鏈接:http://www.tebozhan.com/showinfo-26-12749-0.htmlGo 語言中 panic 和 recover 搭配使用
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: Springboot整合Hutool自定義注解實現數據脫敏
下一篇: 聊聊C#歸并排序算法