Go 專欄|錯誤處理:defer,panic 和 recover
最近校招又開始了,我也接到了一些面試工作,當我問「你覺得自己有什么優(yōu)勢」時,十個人里有八個的回答里會有一條「精力充沛,能加班」。
怪不得國家都給認證了:新生代農(nóng)民工。合著我們這根本就不是什么腦力勞動者,而是靠出賣體力的苦勞力。
好了,廢話不多說,肝文還確實需要體力。
這篇來說說 Go 的錯誤處理。
錯誤處理
錯誤處理相當重要,合理地拋出并記錄錯誤能在排查問題時起到事半功倍的作用。
Go 中有關于錯誤處理的標準模式,即 error 接口,定義如下:
type error interface {
Error() string
}
大部分函數(shù),如果需要返回錯誤的話,基本都會將 error 作為多個返回值的最后一個,舉個例子:
package main
import "fmt"
func main() {
n, err := echo(10)
if err != nil {
fmt.Println("error: " + err.Error())
} else {
fmt.Println(n)
}
}
func echo(param int) (int, error) {
return param, nil
}
我們也可以使用自定義的 error 類型,比如調(diào)用標準庫的 os.Stat 方法,返回的錯誤就是自定義類型:
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
暫時看不懂也沒有關系,等學會了接口之后,再回過頭來看這段代碼,應該就豁然開朗了。
defer
延遲函數(shù)調(diào)用,defer 后邊會接一個函數(shù),但該函數(shù)不會立刻被執(zhí)行,而是等到包含它的程序返回時(包含它的函數(shù)執(zhí)行了 return 語句、運行到函數(shù)結尾自動返回、對應的 goroutine panic),defer 函數(shù)才會被執(zhí)行。
通常用于資源釋放、打印日志、異常捕獲等。
func main() {
f, err := os.Open(filename)
if err != nil {
return err
}
/**
* 這里defer要寫在err判斷的后邊而不是os.Open后邊
* 如果資源沒有獲取成功,就沒有必要對資源執(zhí)行釋放操作
* 如果err不為nil而執(zhí)行資源執(zhí)行釋放操作,有可能導致panic
*/
defer f.Close()
}
defer 語句經(jīng)常成對出現(xiàn),比如打開和關閉,連接和斷開,加鎖和解鎖。
defer 語句在 return 語句之后執(zhí)行。
package main
import (
"fmt"
)
func main() {
fmt.Println(triple(4)) // 12
}
func double(x int) (result int) {
defer func() {
fmt.Printf("double(%d) = %d\n", x, result)
}()
return x + x
}
func triple(x int) (result int) {
defer func() {
result += x
}()
return double(x)
}
切勿在 for 循環(huán)中使用 defer 語句,因為 defer 語句不到函數(shù)的最后一刻是不會執(zhí)行的,所以下面這段代碼很可能會用盡所有文件描述符。
for _, filename := range filenames {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
}
一種解決辦法是將循環(huán)體單獨寫一個函數(shù),這樣每次循環(huán)的時候都會調(diào)用關閉函數(shù)。
for _, filename := range filenames {
if err := doFile(filename); err != nil {
return err
}
}
func doFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
}
defer 語句的執(zhí)行是按調(diào)用 defer 語句的倒序執(zhí)行。
package main
import (
"fmt"
)
func main() {
defer func() {
fmt.Println("first")
}()
defer func() {
fmt.Println("second")
}()
fmt.Println("done")
}
輸出:
done
second
first
panic 和 recover
一般情況下,在程序里記錄錯誤日志,就可以幫助我們在碰到異常時快速定位問題。
但還有一些錯誤比較嚴重的,比如數(shù)組越界訪問,程序會主動調(diào)用 panic 來拋出異常,然后程序退出。
如果不想程序退出的話,可以使用 recover 函數(shù)來捕獲并恢復。
感覺挺不好理解的,但仔細想想其實和 try-catch 也沒什么區(qū)別。
先來看看兩個函數(shù)的定義:
func panic(interface{})
func recover() interface{}
panic 參數(shù)類型是 interface{},所以可以接收任意參數(shù)類型,比如:
panic(404)
panic("network broken")
panic(Error("file not exists"))
recover 需要在 defer 函數(shù)中執(zhí)行,舉個例子:
package main
import (
"fmt"
)
func main() {
G()
}
func G() {
defer func() {
fmt.Println("c")
}()
F()
fmt.Println("繼續(xù)執(zhí)行")
}
func F() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕獲異常:", err)
}
fmt.Println("b")
}()
panic("a")
}
輸出:
捕獲異常: a
b
繼續(xù)執(zhí)行
c
F() 中拋出異常被捕獲,G() 還可以正常繼續(xù)執(zhí)行。如果 F() 沒有捕獲的話,那么 panic 會向上傳遞,直接導致 G() 異常,然后程序直接退出。
還有一個場景就是我們自己在調(diào)試程序時,可以使用 panic 來中斷程序,拋出異常,用于排查問題。
這個就不舉例了,反正是我們自己調(diào)試,怎么爽怎么來就行了。
總結
錯誤處理在開發(fā)過程中至關重要,好的錯誤處理可以使程序更加健壯。而且將錯誤信息清晰地記錄日志,在排查問題時非常有用。
Go 中使用 error 類型進行錯誤處理,還可以在此基礎上自定義錯誤類型。
使用 defer 語句進行延遲調(diào)用,用來關閉或釋放資源。
使用 panic 和 recover 來拋出錯誤和恢復。
使用 panic 一般有兩種情況:
程序遇到無法執(zhí)行的錯誤時,主動調(diào)用
panic結束運行;在調(diào)試程序時,主動調(diào)用
panic結束運行,根據(jù)拋出的錯誤信息來定位問題。
為了程序的健壯性,可以使用 recover 捕獲錯誤,恢復程序運行。
文章中的腦圖和源碼都上傳到了 GitHub,有需要的同學可自行下載。
地址: https://github.com/yongxinz/gopher/tree/main/sc
關注公眾號 AlwaysBeta,回復「goebook」領取 Go 編程經(jīng)典書籍。
Go 專欄文章列表:
