聊聊Go 語言的錯誤處理
構造 error
在 go 語言中,有一個預定義的接口:error,該接口自帶一個 Error() 方法,調用該方法會返回一個字符串。
- type error interface {
- Error() string
- }
調用該方法,會返回當前錯誤的具體結果。一般有下面幾種方式生成 error。
- errors.New()
- fmt.Errorf()
errors.New()
調用 errors.New() 會返回一個 error 類型的結構體,該結構體內部會實現(xiàn)一個 Error() 方法, 調用該方法返回的結果為調用 errors.New() 方法時傳入的內容。
- import (
- "errors"
- "fmt"
- )
- func divide(a, b int) (error, int) {
- if b == 0 {
- // 被除數(shù)為0,則構造一個 error 結構體
- return errors.New("被除數(shù)不能為0"), 0
- }
- var result = a / b
- return nil, result
- }
- func main() {
- var err error // error 類型數(shù)據(jù)的初始值為 nil,類似于 js 中的 null
- var result int
- err, result = divide(1, 0)
- if err == nil {
- // 如果 err 為 nil,說明運行正常
- fmt.Println("計算結果", result)
- } else {
- // 如果 err 不為 nil,說明運行出錯
- // 調用 error 結構體的 Error 方法,輸出錯誤原因
- fmt.Println("計算出錯", err.Error())
- }
- }
可以看到,上面的代碼中,由于調用 divide 除法方法時,由于傳入的被除數(shù)為 0。經(jīng)過判斷,會拋出一個由 errors.New 構造的 error 類型的結構體。
我們將調用 error.Error() 方法返回的結果輸出到控制臺,可以發(fā)現(xiàn)其返回的結果,就是傳入 New 方法的值。
執(zhí)行結果如下:
fmt.Errorf()
通過 fmt.Errorf() 方法構造的 error 結構體,與調用 errors.New() 方法的結果類似。不同的是,fmt.Errorf() 方法會進行一次數(shù)據(jù)的格式化。
- func divide(a, b int) (error, int) {
- if b == 0 {
- // 將參數(shù)進行一次格式化,格式化后的字符串放入 error 中
- return fmt.Errorf("數(shù)據(jù) %d 不合法", b), 0
- }
- var result = a / b
- return nil, result
- }
- err, result := divide(1, 0)
- fmt.Println("計算出錯", err.Error())
執(zhí)行結果如下:
panic() 與 recover()
panic()
panic() 相當于主動停止程序運行,調用時 panic() 時,需要傳入中斷原因。調用后,會在控制臺輸出中斷原因,以及中斷時的調用堆棧。我們可以改造一下之前的代碼:
- func divide(a, b int) (error, int) {
- if b == 0 {
- // 如果程序出錯,直接停止運行
- panic("被除數(shù)不能為0")
- }
- var result = a / b
- return nil, result
- }
- func main() {
- err, result := divide(1, 0)
- fmt.Println("計算出錯", err.Error())
- }
在運行到 panic() 處,程序直接中斷,并在控制臺打印出了中斷原因。
panic() 可以理解為,js 程序中的 throw new Error() 的操作。那么,在 go 中有沒有辦法終止 panic() ,也就是類似于 try-catch 的操作,讓程序回到正常的運行邏輯中呢?
recover()
在介紹 recover() 方法之前,還需要介紹一個 go 語言中的另一個關鍵字:defer。
defer 后的語句會在函數(shù)進行 return 操作之前調用,常用于資源釋放、錯誤捕獲、日志輸出。
- func getData(table, sql) {
- defer 中斷連接()
- db := 建立連接(table)
- data := db.select(sql)
- return data
- }
defer 后的語句會被存儲在一個類似于棧的數(shù)據(jù)結構內,在函數(shù)結束的時候,被定義的語句按順序出棧,越后面定義的語句越先被調用。
- func divide(a, b int) int {
- defer fmt.Println("除數(shù)為", b)
- defer fmt.Println("被除數(shù)為", a)
- result := a / b
- fmt.Println("計算結果為", result)
- return result
- }
- divide(10, 2)
上面的代碼中,我們在函數(shù)開始運行的時候,先通過 defer 定義了兩個輸出語句,先輸出除數(shù),后輸出被除數(shù)。
實際的運行結果是:
- 先輸出計算結果;
- 然后輸出被除數(shù);
- 最后輸出除數(shù);
這和前面提到的,通過 defer 定義的語句會在函數(shù)結束的時候,按照出棧的方式進行執(zhí)行,先定義的后執(zhí)行。defer 除了會在函數(shù)結束的時候執(zhí)行,出現(xiàn)異常的的時候也會先走 defer 的邏輯,也就是說,我們在調用了 panic() 方法后,程序中斷過程中,也會先將 defer 內的語句運行一遍。
這里我們重新定義之前的 divide 函數(shù),在執(zhí)行之前加上一個 defer 語句,defer 后面為一個自執(zhí)行函數(shù),該函數(shù)內會調用 recover() 方法。
recover() 方法調用后,會捕獲到當前的 panic() 拋出的異常,并進行返回,如果沒有異常,則返回 nil。
- func divide(a, b int) int {
- // 中斷之前,調用 defer 后定義的語句
- defer func() {
- if err := recover(); err != nil {
- fmt.Println("捕獲錯誤", err)
- }
- }()
- if b == 0 {
- // 函數(shù)運行被中斷
- panic("被除數(shù)不能為0")
- return 0
- }
- return a / b
- }
- result := divide(1, 0)
- fmt.Println("計算結果", result)
上面的代碼運行后,我們發(fā)現(xiàn)之前調用 panic() 中斷的程序被恢復了,而且后面的計算結果也正常進行輸出了。
這就有點類似于 try-catch 的邏輯了,只是 recover 需要放在 defer 關鍵詞后的語句中,更像是 catch 和 finally 的結合。
本文轉載自微信公眾號「自然醒的筆記本」,可以通過以下二維碼關注。轉載本文請聯(lián)系自然醒的筆記本公眾號。