Go 語言中 panic 和 recover 搭配使用

本次主要聊聊 Go 語言中關(guān)于 panic 和 recover 搭配使用 ,以及 panic 的基本原理

最近工作中審查代碼的時候發(fā)現(xiàn)一段代碼,類似于如下這樣,將 recover 放到一個子協(xié)程里面,期望去捕獲主協(xié)程的程序異常

[圖片上傳失敗...(image-9cb613-1696773595414)]

看到此處,是否會想這段代碼在項(xiàng)目中是想當(dāng)然寫出來的吧,然而平日中,大多問題是出現(xiàn)在認(rèn)知偏差上,那么本次,我們就來消除一下這個認(rèn)知偏差

關(guān)于 Go 語言中顯示的使用 panic 的地方不多,一般 panic ,基本上會出現(xiàn)在咱們程序出現(xiàn)異常退出的時候

例如訪問了空指針里面的值,則會 panic 報(bào)錯無效的內(nèi)存地址,又例如訪問量數(shù)組中不存在的數(shù)組所索引,或者切片索引,那么會報(bào)錯 panic 數(shù)組越界等等

可是碰到這些 panic 的時候,實(shí)際上我們并不期望當(dāng)前的服務(wù)直接掛掉,而是期望這個異常能夠被識別,且不影響程序其他部分的模塊運(yùn)行

正常捕獲異常

在 Go 中可以將 defer 和 recover 進(jìn)行搭配使用,可以捕獲和處理大部分的異常情況,例如可以這樣

[圖片上傳失敗...(image-f8a07e-1696773595414)]

這里可以看到,recover 捕獲異常和發(fā)生異常的部分是在同一個協(xié)程中,實(shí)驗(yàn)證明是可以正常捕獲并且處理異常

并沒有捕獲到異常

  1. 直接不做顯示的 recover,自然 panic 程序崩潰會如期而至,此處我們顯示的使用 panic 函數(shù)來制造恐慌
func main() {
   log.SetFlags(log.Lshortfile)

   panic("panic coming...")

}

[圖片上傳失敗...(image-63359c-1696773595414)]

  1. 不使用 defer 來進(jìn)行處理
func main() {
   log.SetFlags(log.Lshortfile)
    if err := recover(); err != nil {
     log.Println("recover panic : ", err)
    }
   panic("panic coming...")

}

[圖片上傳失敗...(image-233fb-1696773595414)]

自然 recover 函數(shù)是在 panic 調(diào)用之前就已經(jīng)執(zhí)行,此時是還沒有異常需要捕獲和恢復(fù)的,待程序運(yùn)行到 panic 處的時候,實(shí)際上并沒有沒有處理程序崩潰的異常

結(jié)果,仍然是程序崩潰

  1. 當(dāng)然,還有文章開頭提到的出現(xiàn) panic 的位置和捕獲和處理程序崩潰異常的位置不在同一個協(xié)程,自然也是沒法捕獲到的,這一點(diǎn)需要注意,其他的語言可能不是這樣,但是 Go 中是這樣的

panic 基本原理

看了上述現(xiàn)象,實(shí)際上還是對知識點(diǎn)理解得不夠,使用的時候想當(dāng)然了,就像使用 defer 一樣,如果對他不夠了解的話,使用的時候,確實(shí)會出現(xiàn)一些奇奇怪怪的現(xiàn)象,對于 defer 的使用可以查看文末的文章地址

  1. panic 函數(shù)和 recover 函數(shù),Go 源碼builtin\builtin.go中可以看到注釋

[圖片上傳失敗...(image-4e07d0-1696773595414)]

注釋中有說關(guān)于 panic 和 recover 的使用是作用于當(dāng)前協(xié)程的,因此我們使用的時候,如果跨協(xié)程教程使用,自然不會達(dá)到我們期望的效果

  1. 繼續(xù)查看關(guān)于 panic 的源碼,實(shí)際上是一個結(jié)構(gòu),放到 defer 結(jié)構(gòu)里面的一個指針,源碼位置:runtime\runtime2.go

[圖片上傳失敗...(image-874f3d-1696773595414)]

_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
}

上述兩個結(jié)構(gòu)表達(dá)的意思是,程序中出現(xiàn) panic 的時候,實(shí)際上都會創(chuàng)建一個 _panic 結(jié)構(gòu),這個 _panic 結(jié)構(gòu)里面存儲了當(dāng)前程序崩潰的一些必要信息,如下:

  1. argp

是一個 unsafe.Pointer 類型的成員,指向 defer 調(diào)用參數(shù)的指針

  1. arg

出現(xiàn) panic 的原因,如果我們顯示調(diào)用 panic,那么就是我們填入 panic 函數(shù)中的參數(shù),例如上述的 panic coming ...

  1. link

是一個指針,指向上一個,最近的一個 _panic 結(jié)構(gòu)的地址,實(shí)際上此處就可以看到這個指針對應(yīng)的是一個鏈表,一個又多個 _panic 結(jié)構(gòu)組成的鏈表

[圖片上傳失敗...(image-939eca-1696773595414)]

  1. recovered

panic 是否已經(jīng)處理完畢,即當(dāng)前的這個 panic 是否是已經(jīng)被 recover 了

  1. aborted

表示當(dāng)前的 panic 是否被中止

  1. 對于 pc 和 sp 自然就是我們熟知的 pc 通用寄存器,在匯編中是指向當(dāng)前運(yùn)行指令的下一條指令,sp 則是棧指針 stack pointer,用于入棧和出棧的

我們知道運(yùn)行函數(shù)的時候需要入棧,運(yùn)行完畢之后需要出棧

源碼中的 runtime.gopanic

那么我們繼續(xù)來閱讀源碼,上述看到 sp 和 pc ,那么我們就簡單寫一個 panic 的代碼來看看匯編到底是怎么執(zhí)行的,不用擔(dān)心看不懂,我們只需要看關(guān)鍵詞就行

還是上面的程序

[圖片上傳失敗...(image-652e8c-1696773595414)]

程序運(yùn)行的時候可以執(zhí)行 go tool compile -S main.go

可以看到匯編代碼,可能其他的看不懂,但是我們可以看到如下關(guān)鍵詞

[圖片上傳失敗...(image-69dc1b-1696773595414)]

  • log.(*Logger).SetFlags(SB) 即是執(zhí)行到我們調(diào)用 log 去設(shè)置參數(shù)
  • 程序走到 panic 函數(shù)的時候,實(shí)際上是執(zhí)行了 runtime.gopanic 函數(shù),我們一起看看源碼

[圖片上傳失敗...(image-718c6a-1696773595414)]

代碼中可以看到 p.recovered 邏輯下的關(guān)于 recover 的邏輯被刪除掉了,在文章的后面會繼續(xù)說到,當(dāng)前我們先關(guān)注 panic 的事項(xiàng)

runtime.gopanic 程序的邏輯大體是這樣的

  1. 獲取當(dāng)前 協(xié)程 的指針
  1. 初始化一個 _panic 結(jié)構(gòu) p,并將當(dāng)前協(xié)程上對應(yīng)的數(shù)據(jù)賦值給到 p 上,且將 當(dāng)前協(xié)程 _panic 掛到 link 上
  1. 進(jìn)入循環(huán)后,拿到當(dāng)前協(xié)程的 _defer 數(shù)據(jù)
  1. 查看 _defer 指針數(shù)據(jù) 中是否有 defer 調(diào)用,如果有則執(zhí)行
  1. 處理完基本邏輯之后,打印 panic 信息,例如我們 demo 中的panic coming ...信息
  1. 最終退出程序

Xdm 可以看上圖,自己捋一捋邏輯就清晰了

接著,我們來看

fatalpanic

[圖片上傳失敗...(image-f65551-1696773595414)]

通過 runtime.gopanic 我們可以看到 fatalpanic 函數(shù)基本上就是做一個收尾工作了,如果上述程序處理完畢之后, fatalpanic 校驗(yàn)到 panic 是需要 recover 的,那么就打印 [recovered]

打印的這個信息是由 上圖中 printpanics 完成的

[圖片上傳失敗...(image-ae6df8-1696773595414)]

這下知道 panic 是如何去執(zhí)行的了,那么對于現(xiàn)在來研究 recover 是如何落實(shí)的

recover

還是同一個例子,咱們將 defer 部分的代碼注打開,來繼續(xù)看看效果

func main() {
   log.SetFlags(log.Lshortfile)

   defer func() {
      if err := recover(); err != nil {
         log.Println("recover panic : ", err)
      }
   }()

   panic("panic coming...")

}

自然效果是我們期望的,捕獲到了異常,且處理了

[圖片上傳失敗...(image-526ef8-1696773595414)]

繼續(xù)打印匯編來查看一下關(guān)鍵詞,是否有我們期望的函數(shù)出現(xiàn)

[圖片上傳失敗...(image-8c4448-1696773595414)]

[圖片上傳失敗...(image-75bc2d-1696773595414)]

此處我們可以看到,實(shí)際 Go 中調(diào)用了多個函數(shù)

  1. runtime.gorecover
  1. main.main.opendefer
  1. log.(*Logger).SetFlags
  1. runtime.gopanic
  1. runtime.deferreturn

自然明眼人都看的出現(xiàn),關(guān)鍵的函數(shù)實(shí)現(xiàn)自然是 runtime.gorecover ,那么我們來一探究竟

runtime.gorecover

[圖片上傳失敗...(image-4c608d-1696773595414)]

查看源碼我們可以知道, runtime.gorecover 實(shí)際上就是根據(jù)當(dāng)前協(xié)程的 _panic 結(jié)構(gòu)數(shù)據(jù)來判斷是否需要恢復(fù),如果需要則將p.recovered = true

自然在這里將當(dāng)前協(xié)程的數(shù)據(jù)修改掉,正是為了后續(xù)執(zhí)行 runtime.gopanic 的時候提供保障, runtime.gopanic 執(zhí)行的時候就會去判斷和處理這個 p.recovered

前文中提到的關(guān)于 runtime.gopanic 中 處理 p.recovered 的邏輯是這樣的

[圖片上傳失敗...(image-f0d37b-1696773595414)]

[圖片上傳失敗...(image-6569b3-1696773595414)]

  1. 如上可以看到 runtime.gorecover 去對 p.recovered 設(shè)置是否恢復(fù)
  1. runtime.gopanic 中校驗(yàn) p.recovered 已處理,則執(zhí)行 recovery 函數(shù)
  1. recovery 函數(shù)中去處理對應(yīng)的寄存器的值去維護(hù)上下文
  1. 最后我們可以看到最終調(diào)用 gogo 函數(shù)跳回原來調(diào)用的位置

因此,當(dāng)我們在同一個協(xié)程中出現(xiàn)了 panic,且在同一個協(xié)程中去使用 defer 來配合 recover 來進(jìn)行捕獲異常和處理異常,就可以得以實(shí)現(xiàn),看到這里,有沒有覺得還是蠻簡單的,不就是去對一個 p.recovered 進(jìn)行配合處理嗎

自然,表面上是這樣,其中對于寄存器的各種數(shù)據(jù)處理涉及的內(nèi)容還是不少的,不過這不在我們今天聊的范疇中了

總結(jié)

至此,相信你已經(jīng)知道了這些

  1. 為什么 panic 和 defer ,recover 配合使用的時候要在同一個協(xié)程中了吧
  1. 相信你還知道了 panic 和 recover 的處理流程

當(dāng)然,如果文章對你有幫助的話,歡迎留言評論哦

感謝閱讀,歡迎交流,點(diǎn)個贊,關(guān)注一波 再走吧

歡迎點(diǎn)贊,關(guān)注,收藏

朋友們,你的支持和鼓勵,是我堅(jiān)持分享,提高質(zhì)量的動力

[圖片上傳失敗...(image-5452d1-1696773595414)]

技術(shù)是開放的,我們的心態(tài),更應(yīng)是開放的。擁抱變化,向陽而生,努力向前行。

我是阿兵云原生,歡迎點(diǎn)贊關(guān)注收藏,下次見~

文中提到的技術(shù)點(diǎn),感興趣的可以查看這些文章:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容