首頁  >  文章  >  後端開發  >  記錄Golang Recover的一個小坑

記錄Golang Recover的一個小坑

藏色散人
藏色散人轉載
2020-12-22 15:56:503582瀏覽
##上對上由

golang教學專欄為大家一起介紹Golang Recoverover 的一個小坑,希望對需要的朋友有所幫助!

記錄Golang Recover的一個小坑

1.error

#Golang被詬病非常多的一點就是缺少強大方便的異常處理機制,大部分高階程式語言,如Java、PHP、Python等都擁有一種try catch機制,這種異常捕獲機制可以非常方便的處理程序運行中可能出現的各種意外情況。

嚴格來說,在Go裡面,錯誤和異常是2種不同的類型,錯誤一般是指程序產生的邏輯錯誤,或者意料之中的意外情況,而且異常一般就是panic,比如角標越界、段錯誤。

對於錯誤,Golang採用了一個非常原始的手段,我們必須手動處理可能產生的每一個錯誤,一般會把錯誤回傳給呼叫方,下面這種寫法在Go裡面十分常見:

package mainimport (
    "errors"
    "fmt")func main() {
    s, err := say()
    if err != nil {
        fmt.Printf("%s\n", err.Error())
    } else {
        fmt.Printf("%s\n", s)
    }}func say() (string, error) {
    // do something
    return "", errors.New("something error")}复制代码
這種寫法最大的問題就是每一個error都需要判斷處理,非常繁瑣,如果使用try catch機制,我們就可以統一針對多個函數呼叫可能產生的錯誤做處理,節省一點程式碼和時間。不過咱們今天不是來討論Go的異常錯誤處理機制的,這裡只是簡單說一下。

2.panic

一般錯誤都是顯示的,程式明確回傳的,而異常往往是隱示的,不可預測的,例如下面的程式碼:

package mainimport "fmt"func main() {
    fmt.Printf("%d\n", cal(1,2))
    fmt.Printf("%d\n", cal(5,2))
    fmt.Printf("%d\n", cal(5,0)) //panic: runtime error: integer pide by zero 
    fmt.Printf("%d\n", cal(9,5))}func cal(a, b int) int {
    return a / b}复制代码
在執行第三個計算的時候會發生一個panic,這種錯誤會導致程式退出,下面的程式碼的就無法執行了。當然你可以說這種錯誤理論上是可以預測的,我們只要在cal函數內部做好處理就好了。

然而實際開發中,會發生panic的地方可能特別多,而且不是這種一眼就能看出來的,在Web服務中,這樣的panic會導致整個Web服務掛掉,特別危險。

3.recover

雖然沒有try catch機制,Go其實有一種類似的recover機制,功能弱了點,用法很簡單:

package mainimport "fmt"func main() {
    fmt.Printf("%d\n", cal(1, 2))
    fmt.Printf("%d\n", cal(5, 2))
    fmt.Printf("%d\n", cal(5, 0))
    fmt.Printf("%d\n", cal(9, 2))}func cal(a, b int) int {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("%s\n", err)
        }
    }()
    return a / b}复制代码
首先,大家得理解defer的作用,簡單說defer就類似於物件導向裡面的析構函數,在這個函數終止的時候會執行,即使是panic導致的終止。

所以,在cal函數裡面每次終止的時候都會檢查有沒有異常產生,如果產生了我們可以處理,比如說記錄日誌,這樣程式還可以繼續執行下去。

4.注意的坑

一般defer recover這種機制常用在常駐進程的應用,例如Web服務,在Go裡面,每一個Web請求都會分配一個goroutine去處理,在沒有做任何處理的情況下,假如某一個請求發生了panic,就會導致整個服務掛掉,這是不可接受的,所以在Web應用裡面必須使用recover保證即使某一個要求發生錯誤也不影響其它要求。

這裡我使用一小段程式碼模擬一下:

package mainimport (
    "fmt")func main() {
    requests := []int{12, 2, 3, 41, 5, 6, 1, 12, 3, 4, 2, 31}
    for n := range requests {
        go run(n) //开启多个协程
    }

    for {
        select {}
    }}func run(num int) {
    //模拟请求错误
    if num%5 == 0 {
        panic("请求出错")
    }
    fmt.Printf("%d\n", num)}复制代码
上面這段程式碼無法完整執行下去,因為其中某一個協程必然會發生panic,從而導致整個應用程式掛掉,其它協程也停止執行。

解決方法和上面一樣,我們只需要在run函數裡面加入defer recover,整個程式就會非常健壯,即使發生panic,也會完整的執行下去。

func run(num int) {
    defer func() {
        if err := recover();err != nil {
            fmt.Printf("%s\n", err)
        }
    }()
    if num%5 == 0 {
        panic("请求出错")
    }
    fmt.Printf("%d\n", num)}复制代码
上面的程式碼只是演示,真正的坑是:如果你在run函數裡面又啟動了其它協程,這個協程發生的panic是無法被recover的,還是會導致整個進程掛掉,我們改造了一下上面的例子:

func run(num int) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("%s\n", err)
        }
    }()
    if num%5 == 0 {
        panic("请求出错")
    }
    go myPrint(num)}func myPrint(num int) {
    if num%4 == 0 {
        panic("请求又出错了")
    }
    fmt.Printf("%d\n", num)}复制代码
我在run函數裡面又透過協程的方式呼叫了另一個函數,而這個函數也會發生panic,你會發現整個程式也掛了,即使run函式有recover也沒有任何作用,這表示我們還需要在myPrint函式裡面加入recover。但是如果你不使用協程的方式呼叫myPrint函數,直接呼叫的話還是可以捕捉recover的。

總結一下就是defer recover這種機制只是針對當前函數和以及直接調用的函數可能產生的panic,它無法處理其調用產生的其它協程的panic,這一點和try catch機制不一樣。

理論上講,所有使用協程的地方都必須做defer recover處理,這樣才能保證你的應用萬無一失,不過開發中可以根據實際情況而定,對於一些不可能出錯的函數加了還影響性能。

Go的Web服務也是一樣,預設的recover機制只能捕獲一層,如果你在這個請求的處理中又使用了其它協程,那麼必須非常慎重,畢竟只要發生一個panic,整個Web服務就會掛掉。

最後,總結一下,Go的異常處理機制雖然沒有很多其它語言高效,但是基本上還是能滿足需求,目前官方已經在著完善這一點,Go2可能會見到。

推薦:《go語言教學#》

以上是記錄Golang Recover的一個小坑的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除