Go 錯(cuò)誤處理篇(三):panic 和 recover

前面學(xué)院君介紹了 Go 語言通過 error 類型統(tǒng)一進(jìn)行錯(cuò)誤處理,但這些錯(cuò)誤都是我們?cè)诰帉懘a時(shí)就已經(jīng)預(yù)見并返回的,對(duì)于某些運(yùn)行時(shí)錯(cuò)誤,比如數(shù)組越界、除數(shù)為0、空指針引用,這些 Go 語言是怎么處理的呢?
panic
Go 語言沒有像 Java、PHP 那樣引入異常的概念,也沒有提供 try...catch 這樣的語法對(duì)運(yùn)行時(shí)異常進(jìn)行捕獲和處理,當(dāng)代碼運(yùn)行時(shí)出錯(cuò),而又沒有在編碼時(shí)顯式返回錯(cuò)誤時(shí),Go 語言會(huì)拋出 panic,中文譯作「運(yùn)行時(shí)恐慌」,我們也可以將其看作 Go 語言版的異常。
除了像上篇教程演示的那樣由 Go 語言底層拋出 panic,我們還可以在代碼中顯式拋出 panic,以便對(duì)錯(cuò)誤和異常信息進(jìn)行自定義,仍然以上篇教程除數(shù)為 0 的示例代碼為例,我們可以這樣顯式返回 panic 中斷代碼執(zhí)行:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("代碼清理邏輯")
}()
var i = 1
var j = 0
if j == 0 {
panic("除數(shù)不能為0!")
}
k := i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
這樣一來,當(dāng)我們執(zhí)行這段代碼時(shí),就會(huì)拋出 panic:

panic 函數(shù)支持的參數(shù)類型是 interface{}:
func panic(v interface{})
所以可以傳入任意類型的參數(shù):
panic(500) // 傳入數(shù)字
panic(errors.New("除數(shù)不能為0")) // 傳入 error 類型
無論是 Go 語言底層拋出 panic,還是我們?cè)诖a中顯式拋出 panic,處理機(jī)制都是一樣的:當(dāng)遇到 panic 時(shí),Go 語言會(huì)中斷當(dāng)前協(xié)程(即 main 函數(shù))后續(xù)代碼的執(zhí)行,然后執(zhí)行在中斷代碼之前定義的 defer 語句(按照先入后出的順序),最后程序退出并輸出 panic 錯(cuò)誤信息,以及出現(xiàn)錯(cuò)誤的堆棧跟蹤信息,也就是下面紅框中的內(nèi)容:

第一行表示出問題的協(xié)程,第二行是問題代碼所在的包和函數(shù),第三行是問題代碼的具體位置,最后一行則是程序的退出狀態(tài),通過這些信息,可以幫助你快速定位問題并予以解決。
recover
此外,我們還可以通過 recover() 函數(shù)對(duì) panic 進(jìn)行捕獲和處理,從而避免程序崩潰然后直接退出,而是繼續(xù)可以執(zhí)行后續(xù)代碼,實(shí)現(xiàn)類似 Java、PHP 中 try...catch 語句的功能。
由于執(zhí)行到拋出 panic 的問題代碼時(shí),會(huì)中斷后續(xù)其他代碼的執(zhí)行,所以,顯然這個(gè) panic 的捕獲應(yīng)該放到 defer 語句中完成,才可以在拋出 panic 時(shí)通過 recover 函數(shù)將其捕獲,defer 語句執(zhí)行完畢后,會(huì)退出拋出 panic 的當(dāng)前函數(shù),回調(diào)調(diào)用它的地方繼續(xù)后續(xù)代碼的執(zhí)行。
可以類比為 panic、recover、defer 組合起來實(shí)現(xiàn)了傳統(tǒng)面向?qū)ο缶幊坍惓L幚淼?try...catch...finally 功能。
下面我們引入 recover() 函數(shù)來重構(gòu)上述示例代碼如下:
package main
import (
"fmt"
)
func divide() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("Runtime panic caught: %v\n", err)
}
}()
var i = 1
var j = 0
k := i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
func main() {
divide()
fmt.Println("divide 方法調(diào)用完畢,回到 main 函數(shù)")
}
如果沒有通過 recover() 函數(shù)捕獲 panic 的話,程序會(huì)直接崩潰退出,并打印錯(cuò)誤和堆棧信息:

而現(xiàn)在我們?cè)?divide() 函數(shù)的 defer 語句中通過 recover() 函數(shù)捕獲了 panic,并打印捕獲到的錯(cuò)誤信息,這個(gè)時(shí)候,程序會(huì)退出 divide() 函數(shù)而不是整個(gè)應(yīng)用,繼續(xù)執(zhí)行 main() 函數(shù)中的后續(xù)代碼,即恢復(fù)后續(xù)其他代碼的執(zhí)行:

如果在代碼執(zhí)行過程中沒有拋出 panic,比如我們把 divide() 函數(shù)中的 j 值改為 1,則代碼會(huì)正常執(zhí)行到函數(shù)末尾,然后調(diào)用 defer 語句聲明的匿名函數(shù),此時(shí) recover() 函數(shù)返回值為 nil,不會(huì)執(zhí)行 if 分支代碼,然后退出 divide() 函數(shù)回到 main() 函數(shù)執(zhí)行后續(xù)代碼:

這樣一來,當(dāng)程序運(yùn)行過程中拋出 panic 時(shí)我們可以通過 recover() 函數(shù)對(duì)其進(jìn)行捕獲和處理,如果沒有拋出則什么也不做,從而確保了代碼的健壯性。
以上就是 Go 語言錯(cuò)誤和異常處理的全部語法,非常簡單明了。接下來,我們將基于目前已經(jīng)學(xué)習(xí)的基礎(chǔ)語法對(duì) Go 語言編程進(jìn)行優(yōu)化和增強(qiáng) —— 介紹如何通過 Go 代碼實(shí)現(xiàn)常見的數(shù)據(jù)結(jié)構(gòu)和算法,以及如何在 Go 語言中實(shí)現(xiàn)常見的設(shè)計(jì)模式。
(本文完)
學(xué)習(xí)過程中有任何問題,可以通過下面的評(píng)論功能或加入「Go 語言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點(diǎn)擊頁面左下角閱讀原文鏈接查看最新更新的教程。
