大家好,這里是每周都在陪你進步的網管。
之前寫過幾篇關于 Go 錯誤處理的文章,發現文章里不少知識點都有點落伍了,比如Go在1.13后對錯誤處理增加了一些支持,最大的變化就是支持了錯誤包裝(Error Wrapping),以前想要在調用鏈路的函數里包裝錯誤都是用"github.com/pkg/errors"這個庫。
Go 在2019年發布的Go1.13版本也采納了錯誤包裝,并且還提供了幾個很有用的工具函數讓我們能更好地使用包裝錯誤。這篇文章就來主要說一下這方面的知識點,不過開始我們還是再次強調一下使用 Go Error 的誤區,避免我們從其他語言切換過來時給自己后面挖坑。
這一條估計很多人都知道,但是文章開頭開始先從這個慣例開始,因為我以前待過一個PHP轉Go的研發團隊,可能大家一開始都不太會,才有了這種錯誤的使用方式。
首先我們再復述一遍,Go通過error類型的值表示程序里的錯誤。
error類型是一個內建接口類型,該接口只規定了一個返回字符串值的Error方法。
type?error?interface?{
????Error()?string
}
Go程序的函數經常會返回一個error值
package?strconv
func?Atoi(s?string)?(int,?error)?{
??....
}
調用者通過測試error值是否是nil來進行錯誤處理。
i,?err?:=?strconv.Atoi("42")
if?err?!=?nil?{
????fmt.Printf("couldn't?convert?number:?%v\n",?err)
????return
}
fmt.Println("Converted?integer:",?i)
error為nil時表示成功;非nil的error表示失敗。
說完?Go?里?error?最基本的使用方式后,接下來說項目里的自定義錯誤類型。假如項目在 Dao 層定義了一個這樣的錯誤類型來記錄數據庫查詢錯誤。
type?MyError?struct?{
????Sql???string
????Param?string
????Err???error
}
假如,這個自定義的MyError不去實現error接口,Dao 層里的函數返回的都是MyError的話。
func?FindUserRowByPhoneMyError(userId?int)?(user?User,?MyError?error)?{
??......
}
那么使用這些 Dao 函數的代碼邏輯層都得引入dao.MyError這個額外的類型。有人會說,我把MyError定義在公共包里,所有代碼邏輯層、Dao 層都用這個common.MyError總沒啥問題了吧。
使用上乍一看沒什么問題,但其實最大的問題就是不兼容、不符合Go語言對錯誤的接口約束,就沒法對自定義錯誤類型使用Go對error提供的其他功能了,比如說后面要介紹的錯誤包裝。
所以針對自定義的錯誤類型,我們也要讓他變成一個真正的Go error,方法就是讓它實現error接口定義的方法。
func?(e?*MyError)?Error()?string?{
????return?fmt.Sprintf("sql:?%s,?params:?%s,?err:?%s",?e.Sql,?e.Param,?e.Err.Error())
}
在現實的程序應用里,一個邏輯往往要經多多層函數的調用才能完成,那在程序里我們的建議Error Handling 盡量留給上層的調用函數做,中間和底層的函數通過錯誤包裝把自己要記的錯誤信息附加再原始錯誤上再返回給外層函數。
比如像下面這樣
func?doAnotherThing()?error?{
????return?errors.New("error?doing?another?thing")
}
func?doSomething()?error?{
????err?:=?doAnotherThing()
????return?fmt.Errorf("error?doing?something:?%v",?err)
}
func?main()?{
????err?:=?doSomething()
????fmt.Println(err)
}
這段代碼從打印錯誤信息的輸出上看沒什么問題,但是深層次的問題很明顯,我們丟失了原來的err,因為它已經被我們的fmt.Errorf函數轉成一個新的字符串了。
基于這個背景,很多開源三方庫提供了錯誤包裝、追加錯誤調用棧等功能,用的最多的就是"github.com/pkg/errors"這個庫,提供了下面幾個主要的包裝錯誤的功能。
//只附加新的信息
func?WithMessage(err?error,?message?string)?error
//只附加調用堆棧信息
func?WithStack(err?error)?error
//同時附加堆棧和信息
func?Wrap(err?error,?message?string)?error
Go官方在2019年發布1.13版本,自己也增加了對錯誤包裝的支持,不過并沒有提供什么Wrap函數,而是擴展了fmt.Errorf函數,加了一個%w來生成一個包裝錯誤。
e?:=?errors.New("原始錯誤")
w?:=?fmt.Errorf("外面包了一個錯誤%w",?e)
Go1.13引入了包裝錯誤后,同時為內置的errors包添加了3個函數,分別是Unwrap、Is和As。
先來聊聊Unwrap,顧名思義,它的功能就是為了獲取到包裝錯誤里那個被嵌套的error。
func?Unwrap(err?error)?error?{
????//先判斷是否是wrapping?error
?u,?ok?:=?err.(interface?{
??Unwrap()?error
?})
?//如果不是,返回nil
?if?!ok?{
??return?nil
?}
?//否則則調用該error的Unwrap方法返回被嵌套的error
?return?u.Unwrap()
}
這里需要注意的是,嵌套可以有很多層,我們調用一次errors.Unwrap函數只能返回往里一層的error,如果想獲取更里面的,需要調用多次errors.Unwrap函數。最終如果一個error不是warpping error,那么返回的是nil。
如果想得到最原始的error,建議自己封裝個工具函數,類似這樣
func?Cause(err?error)?error?{
?for?err?!=?nil?{
????err?=?errors.Unwrap(err)
?}
?return?err
}
對于我們文章開頭定義的那個自定義錯誤MyError想要把它變成可包裝的Error的話,還需要實現一個Unwrap()方法。
func?(e?*MyError)?Unwrap()?error?{?return?e.Err?}
有了包裝錯誤后,像具體某種錯誤的判斷和錯誤的類型轉換也得需要跟進改一下才行。這就是errors包在1.13后新增的另外兩個工具函數Is和As的作用。接下來我們一個個來說。
在Go 1.13之前沒有包裝錯誤的時候,程序里要判斷是不是同一個error可以直接簡單粗暴的:
if?err?==?os.ErrNotExists?{
??......
}
這樣我們就可以通過判斷來做一些事情。但是現在有了包裝錯誤后這樣辦法就不完美的,因為你根本不知道返回的這個err是不是一個嵌套的error,嵌套了幾層。所以基于這種情況,Go為我們提供了errors.Is函數。
func?Is(err,?target?error)?bool
err和目標錯誤target是同一個,那么返回true。err?是一個包裝錯誤,目標錯誤target也包含在這個嵌套錯誤鏈中的話,那么也返回true。下面是一個使用errors.Is判斷是否是同一錯誤的例子。
var?ErrDivideByZero?=?errors.New("divide?by?zero")
func?Divide(a,?b?int)?(int,?error)?{
????if?b?==?0?{
????????return?0,?ErrDivideByZero
????}
????return?a?/?b,?nil
}
func?main()?{
????a,?b?:=?10,?0
????result,?err?:=?Divide(a,?b)
????if?err?!=?nil?{
????????switch?{
????????case?errors.Is(err,?ErrDivideByZero):
????????????fmt.Println("divide?zero?error")
????????default:
????????????fmt.Printf("unexpected?division?error:?%+v\n",?err)
????????}
????????return
????}
????fmt.Printf("%d?/?%d?=?%d\n",?a,?b,?result)
}
同樣在沒有包裝錯誤前,我們要把error 轉換為一個具體類型的error,一般都是使用類型斷言或者 type switch,其實也就是類型斷言。
if?pathErr,?ok?:=?err.(*os.PathError);?ok?{
?fmt.Println(pathErr.Path)
}
但是有了包裝錯誤之后,返回的err可能是已經被嵌套了,這種方式就不能用了,所以Go為我們在errors包里提供了As函數。
func?As(err?error,?target?interface{})?bool
As?函數所做的就是遍歷錯誤的嵌套鏈,從里面找到類型符合的error,然后把這個error賦給target參數,這樣我們在程序里就可以使用轉換后的target了,因為這里有賦值,所以target必須是一個指針,這個也算是Go內置包里的一個慣例了,像json.Unmarshal也是這樣。
所以把上面的例子用As?函數實現就變成了醬嬸:
var?pathErr?*os.PathError
if?errors.As(err,?pathErr)?{
?fmt.Println(pathErr.Path)
}
這篇文章主要是更新一下Error處理在Go 1.13以后新增的功能點,以前的文章介紹的更多的還是使用"pkg/errors"那個包的方式,主要是前兩年以前公司用的Go版本一直是1.12,所以這部分知識我一直沒更新過來,這里簡單做個梳理。
Go錯誤處理以前也寫過幾篇,建議大家一起看看,有搭建項目的需求時統一看一下。
掃碼關注公眾號「網管叨bi叨」

給網管個星標,第一時間吸我的知識???
網管整理了一本《Go 開發參考書》收集了70多條開發實踐。去公眾號回復【gocookbook】領取!還有一本《k8s 入門實踐》講解了常用軟件在K8s上的部署過程,公眾號回復【k8s】即可領取!
覺得有用就點個在看? ??????