由于Golang的語言設(shè)計(jì)的原因,不管是不是愿意,每個(gè)golang開發(fā)者的幾乎每一段代碼都需要與error做纏斗。下面我就簡單分析一下golang中的error相關(guān)。
轉(zhuǎn)自:https://www.jianshu.com/p/606d0e60c58d
參考:Go語言中文文檔:www.topgoer.com
error是什么?
首先需要明確的一點(diǎn)是,golang中對于error類型的定義是什么?不同于很多語言的exception機(jī)制,golang在語言層面經(jīng)常需要顯示的做錯(cuò)誤處理。其實(shí)從本質(zhì)上來講,golang中的error就是一個(gè)接口:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
和所有接口含義一樣,nil表示零值。
before Go1.13
在golang的1.13版本之前,官方給到的錯(cuò)誤處理方法寥寥無幾,只有用來構(gòu)造無額外參數(shù)的錯(cuò)誤的errors.New和構(gòu)造帶額外參數(shù)的錯(cuò)誤的fmt.Errorf。當(dāng)時(shí),經(jīng)常需要使用標(biāo)準(zhǔn)庫之外的擴(kuò)展庫來支持更豐富發(fā)錯(cuò)誤構(gòu)造和處理,比如由Dave Cheney主導(dǎo)的github.com/pkg/errors。
這些額外的error庫主要的關(guān)注點(diǎn)在于提供方法用于描述錯(cuò)誤的層級。回到上面的錯(cuò)誤本身的定義,只是一個(gè)包含Error方法的接口,本身缺乏對于類似其他語言中類似traceback的描述能力,無法追蹤錯(cuò)誤的詳細(xì)棧信息。
而以github.com/pkg/errors為代表的庫,通過實(shí)現(xiàn)Wrap和Cause方法對來提供了包裝/拆包錯(cuò)誤的能力,提供了類似traceback(但需要開發(fā)者自己定義額外信息)和逐層解析并比較錯(cuò)誤的能力。通過這個(gè)方法對,我們可以實(shí)現(xiàn)下面的用例:
// 為錯(cuò)誤提供更豐富的上下文信息,方便定位錯(cuò)誤
if _, err := ioutil.ReadAll(r);err != nil {
return errors.Wrap(err, "read file failed")
}
// 判斷錯(cuò)誤的根錯(cuò)誤是什么,根據(jù)最初的錯(cuò)誤類型判斷需要走什么錯(cuò)誤處理邏輯
switch err := errors.Cause(err).(type) {
case *io.EOF:
// handle specifically
default:
// unknown error
}
After Go1.13
對于上面描述的錯(cuò)誤處理,相比于較為成熟的exception處理模式,天生缺乏錯(cuò)誤棧信息的缺點(diǎn)讓很多開發(fā)者非常不滿,雖然第三方庫或多或少的彌補(bǔ)了這個(gè)缺點(diǎn),但是作為開發(fā)中占比非常大的一部分代碼,官方庫的缺乏支持還是令人不滿。所以Go team在1.13版本中進(jìn)一步完善了錯(cuò)誤相關(guān)的官方庫支持。
首先,提供了%w構(gòu)造方法和errors.Unwrap的方法對來支持類似Wrap和Cause相關(guān)的能力。
// 為錯(cuò)誤提供更豐富的上下文信息,方便定位錯(cuò)誤
if _, err := ioutil.ReadAll(r);err != nil {
return fmt.Errorf("read file failed with err:%w", err)
}
// 判斷錯(cuò)誤的根錯(cuò)誤是什么,根據(jù)最初的錯(cuò)誤類型判斷需要走什么錯(cuò)誤處理邏輯
rawErr := errors.Unwrap(err)
不僅如此,官方庫還帶來了兩個(gè)錯(cuò)誤比較相關(guān)的API:
if errors.Is(err, io.EOF){
...
}
var eof io.EOF
if errors.As(err, &eof){
...
}
其中,errors.Is方法會逐層調(diào)用Unwrap方法,去和目標(biāo) err做比較,知道沒有Unwrap方法或者err比較成功。errors.As方法的作用類似于之前的針對錯(cuò)誤的類型斷言。
至此,golang官方庫提供了錯(cuò)誤的構(gòu)造方法,錯(cuò)誤的比較方法,額外信息包裝的能力,總體來說應(yīng)該算是比較完善了。
關(guān)于Go1.13錯(cuò)誤處理相關(guān)的實(shí)現(xiàn),可以參考。
夭折的try
另外一個(gè)小小的番外插曲,曾經(jīng)有一個(gè)呼聲頗高的錯(cuò)誤處理相關(guān)的提案:引入try關(guān)鍵字來增強(qiáng)錯(cuò)誤處理的能力。主要使用方法如下:
// 包裝調(diào)用方法
readFile := try(ioutil.ReadAll(r))
...
// 函數(shù)層級統(tǒng)一
defer func(){
if err!=nil{
switch err.(type){
...
}
}
}()
帶來的便利是減少了大量的if err!=nil語句,提供函數(shù)層級的統(tǒng)一錯(cuò)誤處理處(一般在defer處)。然而最后由于可讀性和顯式處理錯(cuò)誤的種種原因,這個(gè)提案被拒絕了。
更近一步的信息可以參考github上相關(guān)的討論 和設(shè)計(jì)文檔。
實(shí)踐
基于go1.13提出的現(xiàn)有錯(cuò)誤處理工具,我們大概能夠采用下面的實(shí)踐來進(jìn)行錯(cuò)誤處理:
- 針對基礎(chǔ)錯(cuò)誤類型,一般通過直接聲明變量或者自定義結(jié)構(gòu):
// 常規(guī)的無額外參數(shù)的error
var BasicErr1 = errors.New("this is a basic error.")
func fn() error{
...
if conditionA{
return BasicErr
}
}
// 調(diào)用處
if err!=nil{
if errors.Is(err, BasicErr1){
...
}
}
// 帶參數(shù)信息的錯(cuò)誤
type CustomErr struct {
Code int64
Msg string
}
func (e CustomErr)Error() string{
return fmt.Sprintf("%d:%s", e.Code, e.Msg)
}
func fn() error{
...
if conditionA{
return CustomErr{Code: 123, Msg: "test"}
}
}
// 調(diào)用處
if err!=nil{
if e,ok:=err.(CustomErr);ok{
...
}
}
- 對于調(diào)用三方庫獲取的報(bào)錯(cuò),一般將額外信息(比如調(diào)用參數(shù),上下文信息等方便定位問題的信息)包裝之后向上層調(diào)用方直接拋出:
if _,err:=ioutil.ReadAll(r);err!=nil{
return fmt.Errorf("read file failed:%w", err)
}
// 調(diào)用方
if err!=nil{
if errors.Is(err, io.EOF){
...
}
}
關(guān)于錯(cuò)誤日志的處理部分,為了防止處處打日志造成的上下文信息分散和大量信息冗余,一般建議的處理方式是對于內(nèi)部方法的調(diào)用,使用%w包裝錯(cuò)誤和必要的額外信息,直接返回到上層;對于最外層方法(一般是http handler或者rpc handler),將錯(cuò)誤包裝上下文,打印到錯(cuò)誤日志中,再使用errors.Is或者errors.As方法,根據(jù)錯(cuò)誤類型進(jìn)行不同的錯(cuò)誤處理邏輯。這樣的好處是,對于全局而言,有且只有最外層一份錯(cuò)誤日志,而這個(gè)錯(cuò)誤信息時(shí)包裝了層層調(diào)用信息的,內(nèi)容最為齊全。