Golang中panic的異常處理

前言

Golang中當程序發生致命異常時(比如數組下標越界,註意這裡的異常並不是error),Golang程序會panic(運行時恐慌)。當程序發生panic時,程序會執行當前棧中的defer 函數列表。然後打印引發panic的具體信息,最後進程退出,本篇文章我們一起探討Golang中的panic以及如何利用defer 和 recover 來恢復這種致命的異常

分析造成panic堆棧信息

func main() {
    f1()
    fmt.Println("main func end")
}

func f1() {
    fmt.Println("func f1 start")
    arr := []int{}
    fmt.Println(arr[10])
    fmt.Println("func f1 end")
}

上述代碼中,我在main函數(主協程)中調用瞭f1函數,在調用完該函數後,我打印瞭「main func end」,程序如果正常執行的話會輸出

func f1 start
func f1 end
main func end

很明顯我們可以看出 f1 函數中,切片arr是沒有索引為10的元素的,這個時候程序運行時會造成panic,下面是程序panic時,console打印的堆棧信息

func f1 start
panic: runtime error: index out of range [10] with length 0
goroutine 1 [running]:
main.f1()
/Users/carlos/go/src/test/demo01.go:15 +0x78
main.main()
/Users/carlos/go/src/test/demo01.go:8 +0x20
Process finished with the exit code 2

我們從堆棧中可以發現:

程序會在造成panic所處的位置終止

我們可以看到錯誤信息中隻輸出瞭 func f1 start

產生panic的原因

panic: runtime error: index out of range [10] with length 0

是哪裡造成的panic

goroutine 1 [running] // 運行該程序的協程
main.f1()
/Users/carlos/go/src/test/demo01.go:15 +0x78 // f1 函數,當前demo01文件的低15行
main.main()
/Users/carlos/go/src/test/demo01.go:8 +0x20 // main 函數,當前文件的弟8行

從上面的panic詳情我們可以看出,錯誤鏈是通過棧的形式展現出來的(mian函數先調用,然後在mian中調用f1),所以大傢以後在程序發生panic時查看堆棧信息時可以先看最上層的錯誤,因為這裡是造成panic的根本原因

如何恢復panic造成的程序崩潰

Golang中提供瞭recover函數用來恢復因panic造成的程序崩潰。recover函數有一個返回值來告訴我們panic產生的具體原因。下面我們通過代碼來進行演示

func main() {
    f1()
    r := recover()
    fmt.Printf("%s \n", r)
    fmt.Println("main func end")
}

func f1() {
    fmt.Println("func f1 start")
    arr := []int{}
    fmt.Println(arr[10])
    fmt.Println("func f1 end")
}

上述代碼中我隻是在調用f1函數的下一行調用瞭recover函數,這樣一來我們的理想狀態瞭能夠恢復程序,讓程序執行完main函數中剩下的代碼(打印panic信息,最後打印 main func end),當我們運行該程序的時候發現recover並沒有起到作用,這是因為當f1造成panic時,f1下方的recover函數根本沒有機會執行。

下面我將上述代碼進行一個簡單的改造:

func main() {
    defer func() {
        fmt.Println("defer func start")
        if r := recover(); r != nil {
            fmt.Printf("%s \n", r)
        }
        fmt.Println("defer func end")
    }()
    f1()
    fmt.Println("main func end")
}

func f1() {
    fmt.Println("func f1 start")
    arr := []int{}
    fmt.Println(arr[10])
    fmt.Println("func f1 end")
}

輸出

func f1 start
defer func start
runtime error: index out of range [10] with length 0 
defer func end

上述代碼中,我隻是在main函數最開頭添加瞭一個defer 函數,並在該函數中調用瞭recover函數。註意,我們在文章的最開頭已經說明瞭,當程序發生panic時,程序會依次執行棧中的defer函數(關於defer函數請閱讀官網描述)。所以當前程序發生panic時在進程退出之前會走到defer函數中執行recover函數,recover函數會恢復當前進程並打印錯誤信息。

這裡我需要特別提醒你一點,最好將defer語句寫在函數的最前面。如果上述例子我將f1的調用寫在defer函數之前,你會發現recover函數還是沒有執行

func main() {
    f1()
    defer func() {
        fmt.Println("defer func start")
        if r := recover(); r != nil {
            fmt.Printf("%s \n", r)
        }
        fmt.Println("defer func end")
    }()
    fmt.Println("main func end")
}

這是因為f1造成panic時,defer函數根本就沒有壓入函數調用棧中。

何時使用panic

當你的項目中特別依賴一些組件時,比如一些web項目中經常會在進程啟動之前初始化一些mysql,mq句柄。這些實例對業務來說是非常重要的,所以當這些實例初始化失敗時我們可以直接讓當前程序panic(手動panic),然後及時發現問題並解決。這樣總比你帶著問題上線後,然後一批流入打入進來,客戶端瘋狂報錯要好

Golang中手動調用panic:

func main() {
    initMysql()
}

func initMysql() {
    panic("init mysql failed") // panic可以接收一個interface類型的參數
}

到此這篇關於Golang中panic的異常處理的文章就介紹到這瞭,更多相關Golang panic內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: