Golang通脈之錯(cuò)誤處理

在實(shí)際工程項(xiàng)目中赶撰,總是通過(guò)程序的錯(cuò)誤信息快速定位問(wèn)題,但是又不希望錯(cuò)誤處理代碼寫的冗余而又啰嗦柱彻。Go語(yǔ)言沒(méi)有提供像Java豪娜、C#語(yǔ)言中的try...catch異常處理方式,而是通過(guò)函數(shù)返回值逐層往上拋哟楷。這種設(shè)計(jì)瘤载,鼓勵(lì)在代碼中顯式的檢查錯(cuò)誤,而非忽略錯(cuò)誤卖擅,好處就是避免漏掉本應(yīng)處理的錯(cuò)誤惕虑。但是帶來(lái)一個(gè)弊端,讓代碼冗余磨镶。

什么是錯(cuò)誤

錯(cuò)誤指的是可能出現(xiàn)問(wèn)題的地方出現(xiàn)了問(wèn)題溃蔫。如打開(kāi)一個(gè)文件時(shí)失敗,這種情況是在意料之中的 琳猫。

而異常指的是不應(yīng)該出現(xiàn)問(wèn)題的地方出現(xiàn)了問(wèn)題伟叛。比如引用了空指針,這種情況在在意料之外的脐嫂⊥彻危可見(jiàn),錯(cuò)誤是業(yè)務(wù)過(guò)程的一部分账千,而異常不是 侥蒙。

Go中的錯(cuò)誤也是一種類型。錯(cuò)誤用內(nèi)置的error 類型表示匀奏。就像其他類型的鞭衩,如int,float64,论衍。錯(cuò)誤值可以存儲(chǔ)在變量中瑞佩,從函數(shù)中返回,等等坯台。

演示錯(cuò)誤

嘗試打開(kāi)一個(gè)不存在的文件:

func main() {  
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
  //根據(jù)f進(jìn)行文件的讀或?qū)?    fmt.Println(f.Name(), "opened successfully")
}

在os包中有打開(kāi)文件的功能函數(shù):

func Open(name string) (file *File, err error)

如果文件已經(jīng)成功打開(kāi)炬丸,那么Open函數(shù)將返回文件處理。如果在打開(kāi)文件時(shí)出現(xiàn)錯(cuò)誤蜒蕾,將返回一個(gè)非nil錯(cuò)誤稠炬。

如果一個(gè)函數(shù)或方法返回一個(gè)錯(cuò)誤,那么按照慣例咪啡,它必須是函數(shù)返回的最后一個(gè)值酸纲。因此,Open 函數(shù)返回的值是最后一個(gè)值瑟匆。

處理錯(cuò)誤的慣用方法是將返回的錯(cuò)誤與nil進(jìn)行比較闽坡。nil值表示沒(méi)有發(fā)生錯(cuò)誤,而非nil值表示出現(xiàn)錯(cuò)誤愁溜。

運(yùn)行結(jié)果:

open /test.txt: No such file or directory

得到一個(gè)錯(cuò)誤疾嗅,說(shuō)明該文件不存在。

錯(cuò)誤類型表示

Go 語(yǔ)言通過(guò)內(nèi)置的錯(cuò)誤接口提供了非常簡(jiǎn)單的錯(cuò)誤處理機(jī)制冕象。

它非常簡(jiǎn)單代承,只有一個(gè) Error 方法用來(lái)返回具體的錯(cuò)誤信息:

type error interface {
    Error() string
}

它包含一個(gè)帶有Error()字符串的方法。任何實(shí)現(xiàn)這個(gè)接口的類型都可以作為一個(gè)錯(cuò)誤使用渐扮。這個(gè)方法提供了對(duì)錯(cuò)誤的描述论悴。

當(dāng)打印錯(cuò)誤時(shí),fmt.Println函數(shù)在內(nèi)部調(diào)用Error() 方法來(lái)獲取錯(cuò)誤的描述墓律。這就是錯(cuò)誤描述是如何在一行中打印出來(lái)的膀估。

從錯(cuò)誤中提取更多信息的不同方法

在上面的例子中,僅僅是打印了錯(cuò)誤的描述耻讽。如果想要的是導(dǎo)致錯(cuò)誤的文件的實(shí)際路徑察纯。一種可能的方法是解析錯(cuò)誤字符串,

open /test.txt: No such file or directory  

可以解析這個(gè)錯(cuò)誤消息并從中獲取文件路徑"/test.txt"针肥。但這是一個(gè)糟糕的方法饼记。在新版本的語(yǔ)言中,錯(cuò)誤描述可以隨時(shí)更改慰枕,代碼將會(huì)中斷具则。

標(biāo)準(zhǔn)Go庫(kù)使用不同的方式提供更多關(guān)于錯(cuò)誤的信息。

斷言底層結(jié)構(gòu)類型具帮,結(jié)構(gòu)字段獲取

如果仔細(xì)閱讀打開(kāi)函數(shù)的文檔博肋,可以看到它返回的是PathError類型的錯(cuò)誤低斋。PathError是一個(gè)struct類型,它在標(biāo)準(zhǔn)庫(kù)中的實(shí)現(xiàn)如下束昵,

type PathError struct {  
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }  

從上面的代碼中,可以理解PathError通過(guò)聲明Error() string方法實(shí)現(xiàn)了錯(cuò)誤接口葛峻。該方法連接操作锹雏、路徑和實(shí)際錯(cuò)誤并返回它。這樣就得到了錯(cuò)誤信息佣耐,

open /test.txt: No such file or directory 

PathError結(jié)構(gòu)的路徑字段包含導(dǎo)致錯(cuò)誤的文件的路徑。修改上面示例唧龄,并打印出路徑:

func main() {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

使用類型斷言獲得錯(cuò)誤接口的基本值兼砖。然后用錯(cuò)誤來(lái)打印路徑.這個(gè)程序輸出,

File at path /test.txt failed to open  

斷言底層結(jié)構(gòu)類型,使用方法獲取

獲得更多信息的第二種方法是斷言底層類型既棺,并通過(guò)調(diào)用struct類型的方法獲取更多信息:

type DNSError struct {  
    ...
}

func (e *DNSError) Error() string {  
    ...
}
func (e *DNSError) Timeout() bool {  
    ... 
}
func (e *DNSError) Temporary() bool {  
    ... 
}

從上面的代碼中可以看到丸冕,DNSError struct有兩個(gè)方法Timeout() bool和Temporary() bool耽梅,它們返回一個(gè)布爾值,表示錯(cuò)誤是由于超時(shí)還是臨時(shí)的佩番。

編寫一個(gè)斷言*DNSError類型的程序众旗,并調(diào)用這些方法來(lái)確定錯(cuò)誤是臨時(shí)的還是超時(shí)的。

func main() {  
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}

在上面的程序中趟畏,嘗試獲取一個(gè)無(wú)效域名的ip地址逝钥,通過(guò)聲明它來(lái)輸入*net.DNSError來(lái)獲得錯(cuò)誤的潛在價(jià)值。

在例子中拱镐,錯(cuò)誤既不是暫時(shí)的艘款,也不是由于超時(shí),因此程序會(huì)打游掷拧:

generic error:  lookup golangbot123.com: no such host  

如果錯(cuò)誤是臨時(shí)的或超時(shí)的哗咆,那么相應(yīng)的If語(yǔ)句就會(huì)執(zhí)行,可以適當(dāng)?shù)靥幚硭?/p>

直接比較

獲得更多關(guān)于錯(cuò)誤的詳細(xì)信息的第三種方法是直接與類型錯(cuò)誤的變量進(jìn)行比較益眉。

filepath包的Glob函數(shù)用于返回與模式匹配的所有文件的名稱晌柬。當(dāng)模式出現(xiàn)錯(cuò)誤時(shí)姥份,該函數(shù)將返回一個(gè)錯(cuò)誤ErrBadPattern

filepath包中定義了ErrBadPattern年碘,如下所述:

var ErrBadPattern = errors.New("syntax error in pattern")  

errors.New()用于創(chuàng)建新的錯(cuò)誤澈歉。

當(dāng)模式出現(xiàn)錯(cuò)誤時(shí),由Glob函數(shù)返回ErrBadPattern

func main() {  
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}

運(yùn)行結(jié)果:

syntax error in pattern  

不要忽略錯(cuò)誤

永遠(yuǎn)不要忽略一個(gè)錯(cuò)誤屿衅。忽視錯(cuò)誤會(huì)招致麻煩埃难。下面一個(gè)示例,列出了與模式匹配的所有文件的名稱涤久,而忽略了錯(cuò)誤處理代碼涡尘。

func main() {  
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}

使用行號(hào)中的空白標(biāo)識(shí)符,忽略了Glob函數(shù)返回的錯(cuò)誤:

matched files []  

由于忽略了這個(gè)錯(cuò)誤响迂,輸出看起來(lái)好像沒(méi)有文件匹配到這個(gè)模式考抄,但是實(shí)際上這個(gè)模式本身是畸形的。所以不要忽略錯(cuò)誤蔗彤。

自定義錯(cuò)誤

創(chuàng)建自定義錯(cuò)誤可以使用errors包下的New()函數(shù)川梅,以及fmt包下的:Errorf()函數(shù)。

//errors包:
func New(text string) error {}

//fmt包:
func Errorf(format string, a ...interface{}) error {}

下面提供了錯(cuò)誤包中的新功能的實(shí)現(xiàn)然遏。

// Package errors implements functions to manipulate errors.
  package errors

  // New returns an error that formats as the given text.
  func New(text string) error {
      return &errorString{text}
  }

  // errorString is a trivial implementation of error.
  type errorString struct {
      s string
  }

  func (e *errorString) Error() string {
      return e.s
  }

既然知道了New()函數(shù)是如何工作的挑势,那么就使用它來(lái)創(chuàng)建一個(gè)自定義錯(cuò)誤。

創(chuàng)建一個(gè)簡(jiǎn)單的程序啦鸣,計(jì)算一個(gè)圓的面積潮饱,如果半徑為負(fù),將返回一個(gè)錯(cuò)誤诫给。

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius is less than zero 

使用Errorf向錯(cuò)誤添加更多信息

上面的程序運(yùn)行沒(méi)有問(wèn)題香拉,但是如果要打印出導(dǎo)致錯(cuò)誤的實(shí)際半徑,就不好處理了中狂。這就是fmt包的Errorf函數(shù)的用武之地凫碌。這個(gè)函數(shù)根據(jù)一個(gè)格式說(shuō)明器格式化錯(cuò)誤,并返回一個(gè)字符串作為值來(lái)滿足錯(cuò)誤胃榕。

使用Errorf函數(shù)盛险,修改程序:

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius -20.00 is less than zero  

使用結(jié)構(gòu)體和字段提供錯(cuò)誤的更多信息

還可以使用將錯(cuò)誤接口實(shí)現(xiàn)為錯(cuò)誤的struct類型。這使得錯(cuò)誤處理更加的靈活勋又。在上述示例中苦掘,如果想要訪問(wèn)導(dǎo)致錯(cuò)誤的半徑,那么唯一的方法是解析錯(cuò)誤描述區(qū)域計(jì)算失敗楔壤,半徑-20.00小于零鹤啡。這不是一種正確的方法,因?yàn)槿绻枋霭l(fā)生了變化蹲嚣,那么代碼就會(huì)中斷递瑰。

前面提到“斷言底層結(jié)構(gòu)類型從struct字段獲取更多信息”祟牲,并使用struct字段來(lái)提供對(duì)導(dǎo)致錯(cuò)誤的半徑的訪問(wèn)《恫浚可以創(chuàng)建一個(gè)實(shí)現(xiàn)錯(cuò)誤接口的struct類型说贝,并使用它的字段來(lái)提供關(guān)于錯(cuò)誤的更多信息。

1慎颗、創(chuàng)建一個(gè)struct類型來(lái)表示錯(cuò)誤乡恕。錯(cuò)誤類型的命名約定是,名稱應(yīng)該以文本Error結(jié)束:

type areaError struct {  
    err    string
    radius float64
}

上面的struct類型有一個(gè)字段半徑哗总,它存儲(chǔ)了為錯(cuò)誤負(fù)責(zé)的半徑的值几颜,并且錯(cuò)誤字段存儲(chǔ)了實(shí)際的錯(cuò)誤消息倍试。

2讯屈、實(shí)現(xiàn)error 接口

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

在上面的代碼片段中,使用一個(gè)指針接收器區(qū)域錯(cuò)誤來(lái)實(shí)現(xiàn)錯(cuò)誤接口的Error() string方法县习。這個(gè)方法打印出半徑和錯(cuò)誤描述涮母。

type areaError struct {  
    err    string
    radius float64
}

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            fmt.Printf("Radius %0.2f is less than zero", err.radius)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

程序輸出:

Radius -20.00 is less than zero

使用結(jié)構(gòu)體方法提供錯(cuò)誤的更多信息

1、創(chuàng)建一個(gè)結(jié)構(gòu)來(lái)表示錯(cuò)誤躁愿。

type areaError struct {  
    err    string //error description
    length float64 //length which caused the error
    width  float64 //width which caused the error
}

上面的錯(cuò)誤結(jié)構(gòu)類型包含一個(gè)錯(cuò)誤描述字段叛本,以及導(dǎo)致錯(cuò)誤的長(zhǎng)度和寬度。

2彤钟、實(shí)現(xiàn)錯(cuò)誤接口来候,并在錯(cuò)誤類型上添加一些方法來(lái)提供關(guān)于錯(cuò)誤的更多信息。

func (e *areaError) Error() string {  
    return e.err
}

func (e *areaError) lengthNegative() bool {  
    return e.length < 0
}

func (e *areaError) widthNegative() bool {  
    return e.width < 0
}

在上面的代碼片段中逸雹,返回Error() string 方法的錯(cuò)誤描述营搅。當(dāng)長(zhǎng)度小于0時(shí),lengthNegative() bool方法返回true;當(dāng)寬度小于0時(shí)梆砸,widthNegative() bool方法返回true转质。這兩種方法提供了更多關(guān)于誤差的信息。

面積計(jì)算函數(shù):

func rectArea(length, width float64) (float64, error) {  
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

上面的rectArea函數(shù)檢查長(zhǎng)度或?qū)挾仁欠裥∮?帖世,如果它返回一個(gè)錯(cuò)誤消息休蟹,則返回矩形的面積為nil。

主函數(shù):

func main() {  
    length, width := -5.0, -9.0
    area, err := rectArea(length, width)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            if err.lengthNegative() {
                fmt.Printf("error: length %0.2f is less than zero\n", err.length)

            }
            if err.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", err.width)

            }
        }
        fmt.Println(err)
        return
    }
    fmt.Println("area of rect", area)
}

運(yùn)行結(jié)果:

error: length -5.00 is less than zero  
error: width -9.00 is less than zero 

錯(cuò)誤斷言

有了自定義的 error日矫,并且攜帶了更多的錯(cuò)誤信息后赂弓,就可以使用這些信息了。需要先把返回的 error 接口轉(zhuǎn)換為自定義的錯(cuò)誤類型哪轿,即類型斷言拣展。

下面代碼中的 err.(*commonError) 就是類型斷言在 error 接口上的應(yīng)用,也可以稱為 error 斷言缔逛。

sum, err := add(-1, 2)
if cm,ok := err.(*commonError);ok{
   fmt.Println("錯(cuò)誤代碼為:",cm.errorCode,"备埃,錯(cuò)誤信息為:",cm.errorMsg)
} else {
   fmt.Println(sum)
}

如果返回的 ok 為 true姓惑,說(shuō)明 error 斷言成功,正確返回了 *commonError 類型的變量 cm按脚,所以就可以像示例中一樣使用變量 cm 的 errorCode 和 errorMsg 字段信息了于毙。

錯(cuò)誤嵌套

Error Wrapping

error 接口雖然比較簡(jiǎn)潔,但是功能也比較弱辅搬。想象一下唯沮,假如有這樣的需求:基于一個(gè)存在的 error 再生成一個(gè) error,需要怎么做呢堪遂?這就是錯(cuò)誤嵌套介蛉。

這種需求是存在的,比如調(diào)用一個(gè)函數(shù)溶褪,返回了一個(gè)錯(cuò)誤信息 error币旧,在不想丟失這個(gè) error 的情況下,又想添加一些額外信息返回新的 error猿妈。這時(shí)候吹菱,首先想到的應(yīng)該是自定義一個(gè) struct,如下面的代碼所示:

type MyError struct {
    err error
    msg string
}

這個(gè)結(jié)構(gòu)體有兩個(gè)字段彭则,其中 error 類型的 err 字段用于存放已存在的 error鳍刷,string 類型的 msg 字段用于存放新的錯(cuò)誤信息,這種方式就是 error 的嵌套俯抖。

現(xiàn)在讓 MyError 這個(gè) struct 實(shí)現(xiàn) error 接口输瓜,然后在初始化 MyError 的時(shí)候傳遞存在的 error 和新的錯(cuò)誤信息:

func (e *MyError) Error() string {
    return e.err.Error() + e.msg
}
func main() {
    //err是一個(gè)存在的錯(cuò)誤,可以從另外一個(gè)函數(shù)返回
    newErr := MyError{err, "數(shù)據(jù)上傳問(wèn)題"}
}

這種方式可以滿足需求芬萍,但是非常煩瑣尤揣,因?yàn)榧纫x新的類型還要實(shí)現(xiàn) error 接口。所以從 Go 語(yǔ)言 1.13 版本開(kāi)始担忧,Go 標(biāo)準(zhǔn)庫(kù)新增了 Error Wrapping 功能芹缔,可以基于一個(gè)存在的 error 生成新的 error,并且可以保留原 error 信息:

e := errors.New("原始錯(cuò)誤e")
w := fmt.Errorf("Wrap了一個(gè)錯(cuò)誤:%w", e)
fmt.Println(w)

Go 語(yǔ)言沒(méi)有提供 Wrap 函數(shù)瓶盛,而是擴(kuò)展了 fmt.Errorf 函數(shù)最欠,然后加了一個(gè) %w,通過(guò)這種方式惩猫,便可以生成 wrapping error芝硬。

errors.Unwrap 函數(shù)

既然 error 可以包裹嵌套生成一個(gè)新的 error,那么也可以被解開(kāi)轧房,即通過(guò) errors.Unwrap 函數(shù)得到被嵌套的 error拌阴。

Go 語(yǔ)言提供了 errors.Unwrap 用于獲取被嵌套的 error,比如以上例子中的錯(cuò)誤變量 w 奶镶,就可以對(duì)它進(jìn)行 unwrap迟赃,獲取被嵌套的原始錯(cuò)誤 e:

fmt.Println(errors.Unwrap(w))

可以看到這樣的信息陪拘,即“原始錯(cuò)誤 e”。

原始錯(cuò)誤e

errors.Is 函數(shù)

有了 Error Wrapping 后纤壁,會(huì)發(fā)現(xiàn)原來(lái)用的判斷兩個(gè) error 是不是同一個(gè) error 的方法失效了左刽,比如 Go 語(yǔ)言標(biāo)準(zhǔn)庫(kù)經(jīng)常用到的如下代碼中的方式:

if err == os.ErrExist

為什么會(huì)出現(xiàn)這種情況呢?由于 Go 語(yǔ)言的 Error Wrapping 功能酌媒,令人不知道返回的 err 是否被嵌套欠痴,又嵌套了幾層?

于是 Go 語(yǔ)言為我們提供了 errors.Is 函數(shù)秒咨,用來(lái)判斷兩個(gè) error 是否是同一個(gè):

func Is(err, target error) bool

可以解釋為:

如果 err 和 target 是同一個(gè)喇辽,那么返回 true。

如果 err 是一個(gè) wrapping error雨席,target 也包含在這個(gè)嵌套 error 鏈中的話菩咨,也返回 true

可以簡(jiǎn)單地概括為舅世,兩個(gè) error 相等或 err 包含 target 的情況下返回 true旦委,其余返回 false奇徒。用上面的示例判斷錯(cuò)誤 w 中是否包含錯(cuò)誤 e:

fmt.Println(errors.Is(w,e))

errors.As 函數(shù)

同樣的原因雏亚,有了 error 嵌套后,error 斷言也不能用了摩钙,因?yàn)椴恢酪粋€(gè) error 是否被嵌套罢低,又嵌套了幾層。所以 Go 語(yǔ)言為解決這個(gè)問(wèn)題提供了 errors.As 函數(shù)胖笛,比如前面 error 斷言的例子网持,可以使用 errors.As 函數(shù)重寫,效果是一樣的:

var cm *commonError
if errors.As(err,&cm){
   fmt.Println("錯(cuò)誤代碼為:",cm.errorCode,"长踊,錯(cuò)誤信息為:",cm.errorMsg)
} else {
   fmt.Println(sum)
}

所以在 Go 語(yǔ)言提供的 Error Wrapping 能力下功舀,要盡可能地使用 Is、As 這些函數(shù)做判斷和轉(zhuǎn)換身弊。

Deferred 函數(shù)

在一個(gè)自定義函數(shù)中辟汰,打開(kāi)了一個(gè)文件,然后需要關(guān)閉它以釋放資源阱佛。不管代碼執(zhí)行了多少分支帖汞,是否出現(xiàn)了錯(cuò)誤,文件是一定要關(guān)閉的凑术,這樣才能保證資源的釋放翩蘸。

如果這個(gè)事情由開(kāi)發(fā)人員來(lái)做,隨著業(yè)務(wù)邏輯的復(fù)雜會(huì)變得非常麻煩淮逊,而且還有可能會(huì)忘記關(guān)閉催首》鲇唬基于這種情況,Go 語(yǔ)言提供了 defer 函數(shù)郎任,可以保證文件關(guān)閉后一定會(huì)被執(zhí)行姻檀,不管自定義的函數(shù)出現(xiàn)異常還是錯(cuò)誤。

下面的代碼是 Go 語(yǔ)言標(biāo)準(zhǔn)包 ioutil 中的 ReadFile 函數(shù)涝滴,它需要打開(kāi)一個(gè)文件绣版,然后通過(guò) defer 關(guān)鍵字確保在 ReadFile 函數(shù)執(zhí)行結(jié)束后,f.Close() 方法被執(zhí)行歼疮,這樣文件的資源才一定會(huì)釋放杂抽。

func ReadFile(filename string) ([]byte, error) {
   f, err := os.Open(filename)
   if err != nil {
      return nil, err
   }
   defer f.Close()
   //省略無(wú)關(guān)代碼
   return readAll(f, n)
}

defer 關(guān)鍵字用于修飾一個(gè)函數(shù)或者方法,使得該函數(shù)或者方法在返回前才會(huì)執(zhí)行韩脏,也就說(shuō)被延遲缩麸,但又可以保證一定會(huì)執(zhí)行。

以上面的 ReadFile 函數(shù)為例赡矢,被 defer 修飾的 f.Close 方法延遲執(zhí)行杭朱,也就是說(shuō)會(huì)先執(zhí)行 readAll(f, n),然后在整個(gè) ReadFile 函數(shù) return 之前執(zhí)行 f.Close 方法吹散。

defer 語(yǔ)句常被用于成對(duì)的操作弧械,如文件的打開(kāi)和關(guān)閉,加鎖和釋放鎖空民,連接的建立和斷開(kāi)等刃唐。不管多么復(fù)雜的操作,都可以保證資源被正確地釋放界轩。

panic()和recover()

Golang中引入兩個(gè)內(nèi)置函數(shù)panicrecover來(lái)觸發(fā)和終止異常處理流程画饥,同時(shí)引入關(guān)鍵字defer來(lái)延遲執(zhí)行defer后面的函數(shù)。 一直等到包含defer語(yǔ)句的函數(shù)執(zhí)行完畢時(shí)浊猾,延遲函數(shù)(defer后的函數(shù))才會(huì)被執(zhí)行抖甘,而不管包含defer語(yǔ)句的函數(shù)是通過(guò)return的正常結(jié)束,還是由于panic導(dǎo)致的異常結(jié)束葫慎。你可以在一個(gè)函數(shù)中執(zhí)行多條defer語(yǔ)句衔彻,它們的執(zhí)行順序與聲明順序相反。 當(dāng)程序運(yùn)行時(shí)幅疼,如果遇到引用空指針米奸、下標(biāo)越界或顯式調(diào)用panic函數(shù)等情況,則先觸發(fā)panic函數(shù)的執(zhí)行爽篷,然后調(diào)用延遲函數(shù)悴晰。調(diào)用者繼續(xù)傳遞panic,因此該過(guò)程一直在調(diào)用棧中重復(fù)發(fā)生:函數(shù)停止執(zhí)行,調(diào)用延遲執(zhí)行函數(shù)等铡溪。如果一路在延遲函數(shù)中沒(méi)有recover函數(shù)的調(diào)用漂辐,則會(huì)到達(dá)該協(xié)程的起點(diǎn),該協(xié)程結(jié)束棕硫,然后終止其他所有協(xié)程髓涯,包括主協(xié)程(類似于C語(yǔ)言中的主線程,該協(xié)程ID為1)哈扮。

panic:

  1. 內(nèi)建函數(shù)
  2. 假如函數(shù)F中書(shū)寫了panic語(yǔ)句纬纪,會(huì)終止其后要執(zhí)行的代碼,在panic所在函數(shù)F內(nèi)如果存在要執(zhí)行的defer函數(shù)列表滑肉,按照defer的逆序執(zhí)行
  3. 返回函數(shù)F的調(diào)用者G包各,在G中,調(diào)用函數(shù)F語(yǔ)句之后的代碼不會(huì)執(zhí)行靶庙,假如函數(shù)G中存在要執(zhí)行的defer函數(shù)列表问畅,按照defer的逆序執(zhí)行,這里的defer 有點(diǎn)類似 try-catch-finally 中的 finally
  4. 直到goroutine整個(gè)退出六荒,并報(bào)告錯(cuò)誤

recover:

  1. 內(nèi)建函數(shù)
  2. 用來(lái)控制一個(gè)goroutine的panic行為护姆,捕獲panic,從而影響應(yīng)用的行為
  3. 一般的調(diào)用建議 a). 在defer函數(shù)中掏击,通過(guò)recever來(lái)終止一個(gè)goroutine的panic過(guò)程卵皂,從而恢復(fù)正常代碼的執(zhí)行 b). 可以獲取通過(guò)panic傳遞的error

簡(jiǎn)單來(lái)講:Go中可以拋出一個(gè)panic的異常,然后在defer中通過(guò)recover捕獲這個(gè)異常铐料,然后正常處理渐裂。

錯(cuò)誤和異常從Golang機(jī)制上講豺旬,就是error和panic的區(qū)別钠惩。很多其他語(yǔ)言也一樣,比如C++/Java族阅,沒(méi)有error但有errno篓跛,沒(méi)有panic但有throw。

Golang錯(cuò)誤和異常是可以互相轉(zhuǎn)換的:

  1. 錯(cuò)誤轉(zhuǎn)異常坦刀,比如程序邏輯上嘗試請(qǐng)求某個(gè)URL愧沟,最多嘗試三次,嘗試三次的過(guò)程中請(qǐng)求失敗是錯(cuò)誤鲤遥,嘗試完第三次還不成功的話沐寺,失敗就被提升為異常了。
  2. 異常轉(zhuǎn)錯(cuò)誤盖奈,比如panic觸發(fā)的異常被recover恢復(fù)后混坞,將返回值中error類型的變量進(jìn)行賦值,以便上層函數(shù)繼續(xù)走錯(cuò)誤處理流程。

什么情況下用錯(cuò)誤表達(dá)究孕,什么情況下用異常表達(dá)啥酱,就得有一套規(guī)則,否則很容易出現(xiàn)一切皆錯(cuò)誤或一切皆異常的情況厨诸。

以下給出異常處理的作用域(場(chǎng)景):

  1. 空指針引用
  2. 下標(biāo)越界
  3. 除數(shù)為0
  4. 不應(yīng)該出現(xiàn)的分支镶殷,比如default
  5. 輸入不應(yīng)該引起函數(shù)錯(cuò)誤

其他場(chǎng)景使用錯(cuò)誤處理,這使得函數(shù)接口很精煉微酬。對(duì)于異常绘趋,可以選擇在一個(gè)合適的上游去recover,并打印堆棧信息颗管,使得部署后的程序不會(huì)終止埋心。

說(shuō)明: Golang錯(cuò)誤處理方式一直是很多人詬病的地方,有些人吐槽說(shuō)一半的代碼都是"if err != nil { / 打印 && 錯(cuò)誤處理 / }"忙上,嚴(yán)重影響正常的處理邏輯拷呆。當(dāng)我們區(qū)分錯(cuò)誤和異常,根據(jù)規(guī)則設(shè)計(jì)函數(shù)疫粥,就會(huì)大大提高可讀性和可維護(hù)性茬斧。

錯(cuò)誤處理的正確姿勢(shì)

姿勢(shì)一:失敗的原因只有一個(gè)時(shí),不使用error

func (self *AgentContext) CheckHostType(host_type string) error {
    switch host_type {
    case "virtual_machine":
        return nil
    case "bare_metal":
        return nil
    }
    return errors.New("CheckHostType ERROR:" + host_type)
}

該函數(shù)失敗的原因只有一個(gè)梗逮,所以返回值的類型應(yīng)該為bool项秉,而不是error,重構(gòu)一下代碼:

func (self *AgentContext) IsValidHostType(hostType string) bool {
    return hostType == "virtual_machine" || hostType == "bare_metal"
}

說(shuō)明:大多數(shù)情況慷彤,導(dǎo)致失敗的原因不止一種娄蔼,尤其是對(duì)I/O操作而言,用戶需要了解更多的錯(cuò)誤信息底哗,這時(shí)的返回值類型不再是簡(jiǎn)單的bool岁诉,而是error

姿勢(shì)二:沒(méi)有失敗時(shí),不使用error

error在Golang中是如此的流行跋选,以至于很多人設(shè)計(jì)函數(shù)時(shí)不管三七二十一都使用error涕癣,即使沒(méi)有一個(gè)失敗原因:

func (self *CniParam) setTenantId() error {
    self.TenantId = self.PodNs
    return nil
}

對(duì)于上面的函數(shù)設(shè)計(jì),就會(huì)有下面的調(diào)用代碼:

err := self.setTenantId()
if err != nil {
    // log
    // free resource
    return errors.New(...)
}

重構(gòu)一下代碼:

func (self *CniParam) setTenantId() {
    self.TenantId = self.PodNs
}

于是調(diào)用代碼變?yōu)椋?/p>

self.setTenantId()

姿勢(shì)三:error應(yīng)放在返回值類型列表的最后

對(duì)于返回值類型error前标,用來(lái)傳遞錯(cuò)誤信息坠韩,在Golang中通常放在最后一個(gè)。

resp, err := http.Get(url)
if err != nil {
    return nill, err
}

bool作為返回值類型時(shí)也一樣炼列。

value, ok := cache.Lookup(key) 
if !ok {
    // ...cache[key] does not exist… 
}

姿勢(shì)四:錯(cuò)誤值統(tǒng)一定義只搁,而不是跟著感覺(jué)走

很多人寫代碼時(shí),到處return errors.New(value)俭尖,而錯(cuò)誤value在表達(dá)同一個(gè)含義時(shí)也可能形式不同氢惋,比如“記錄不存在”的錯(cuò)誤value可能為:

  1. "record is not existed."
  2. "record is not exist!"
  3. "record is not existed!!明肮!"
  4. ...

這使得相同的錯(cuò)誤value撒在一大片代碼里菱农,當(dāng)上層函數(shù)要對(duì)特定錯(cuò)誤value進(jìn)行統(tǒng)一處理時(shí),需要漫游所有下層代碼柿估,以保證錯(cuò)誤value統(tǒng)一循未,不幸的是有時(shí)會(huì)有漏網(wǎng)之魚(yú),而且這種方式嚴(yán)重阻礙了錯(cuò)誤value的重構(gòu)秫舌。

于是的妖,可以參考C/C++的錯(cuò)誤碼定義文件,在Golang的每個(gè)包中增加一個(gè)錯(cuò)誤對(duì)象定義文件足陨,如下所示:

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

姿勢(shì)五:錯(cuò)誤逐層傳遞時(shí)嫂粟,層層都加日志

層層都加日志非常方便故障定位。

說(shuō)明:至于通過(guò)測(cè)試來(lái)發(fā)現(xiàn)故障墨缘,而不是日志星虹,目前很多團(tuán)隊(duì)還很難做到。如果你或你的團(tuán)隊(duì)能做到镊讼,那么請(qǐng)忽略這個(gè)姿勢(shì)宽涌。

姿勢(shì)六:錯(cuò)誤處理使用defer

一般通過(guò)判斷error的值來(lái)處理錯(cuò)誤,如果當(dāng)前操作失敗蝶棋,需要將本函數(shù)中已經(jīng)create的資源destroy掉卸亮,示例代碼如下:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    err = createResource2()
    if err != nil {
        destroyResource1()
        return ERR_CREATE_RESOURCE2_FAILED
    }

    err = createResource3()
    if err != nil {
        destroyResource1()
        destroyResource2()
        return ERR_CREATE_RESOURCE3_FAILED
    }

    err = createResource4()
    if err != nil {
        destroyResource1()
        destroyResource2()
        destroyResource3()
        return ERR_CREATE_RESOURCE4_FAILED
    } 
    return nil
}

當(dāng)Golang的代碼執(zhí)行時(shí),如果遇到defer的閉包調(diào)用玩裙,則壓入堆棧兼贸。當(dāng)函數(shù)返回時(shí),會(huì)按照后進(jìn)先出的順序調(diào)用閉包吃溅。 對(duì)于閉包的參數(shù)是值傳遞溶诞,而對(duì)于外部變量卻是引用傳遞,所以閉包中的外部變量err的值就變成外部函數(shù)返回時(shí)最新的err值罕偎。 根據(jù)這個(gè)結(jié)論很澄,重構(gòu)上面的示例代碼:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource1()
        }
    }()
    err = createResource2()
    if err != nil {
        return ERR_CREATE_RESOURCE2_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource2()
                   }
    }()

    err = createResource3()
    if err != nil {
        return ERR_CREATE_RESOURCE3_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource3()
        }
    }()

    err = createResource4()
    if err != nil {
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}

姿勢(shì)七:當(dāng)嘗試幾次可以避免失敗時(shí),不要立即返回錯(cuò)誤

如果錯(cuò)誤的發(fā)生是偶然性的颜及,或由不可預(yù)知的問(wèn)題導(dǎo)致。一個(gè)明智的選擇是重新嘗試失敗的操作蹂楣,有時(shí)第二次或第三次嘗試時(shí)會(huì)成功俏站。在重試時(shí),我們需要限制重試的時(shí)間間隔或重試的次數(shù)痊土,防止無(wú)限制的重試肄扎。

兩個(gè)案例:

  1. 我們平時(shí)上網(wǎng)時(shí),嘗試請(qǐng)求某個(gè)URL,有時(shí)第一次沒(méi)有響應(yīng)犯祠,當(dāng)我們?cè)俅嗡⑿聲r(shí)法严,就有了驚喜俊柔。
  2. 團(tuán)隊(duì)的一個(gè)QA曾經(jīng)建議當(dāng)Neutron的attach操作失敗時(shí),最好嘗試三次,這在當(dāng)時(shí)的環(huán)境下驗(yàn)證果然是有效的挥等。

姿勢(shì)八:當(dāng)上層函數(shù)不關(guān)心錯(cuò)誤時(shí),建議不返回error

對(duì)于一些資源清理相關(guān)的函數(shù)(destroy/delete/clear)牍疏,如果子函數(shù)出錯(cuò)箍铲,打印日志即可,而無(wú)需將錯(cuò)誤進(jìn)一步反饋到上層函數(shù)梨睁,因?yàn)橐话闱闆r下鲸睛,上層函數(shù)是不關(guān)心執(zhí)行結(jié)果的,或者即使關(guān)心也無(wú)能為力坡贺,于是我們建議將相關(guān)函數(shù)設(shè)計(jì)為不返回error官辈。

姿勢(shì)九:當(dāng)發(fā)生錯(cuò)誤時(shí),不忽略有用的返回值

通常遍坟,當(dāng)函數(shù)返回non-nil的error時(shí)钧萍,其他的返回值是未定義的(undefined),這些未定義的返回值應(yīng)該被忽略政鼠。然而风瘦,有少部分函數(shù)在發(fā)生錯(cuò)誤時(shí),仍然會(huì)返回一些有用的返回值公般。比如万搔,當(dāng)讀取文件發(fā)生錯(cuò)誤時(shí),Read函數(shù)會(huì)返回可以讀取的字節(jié)數(shù)以及錯(cuò)誤信息官帘。對(duì)于這種情況瞬雹,應(yīng)該將讀取到的字符串和錯(cuò)誤信息一起打印出來(lái)。

說(shuō)明:對(duì)函數(shù)的返回值要有清晰的說(shuō)明刽虹,以便于其他人使用酗捌。

異常處理的正確姿勢(shì)

姿勢(shì)一:在程序開(kāi)發(fā)階段,堅(jiān)持速錯(cuò)

速錯(cuò)涌哲,簡(jiǎn)單來(lái)講就是“讓它掛”胖缤,只有掛了你才會(huì)第一時(shí)間知道錯(cuò)誤。在早期開(kāi)發(fā)以及任何發(fā)布階段之前阀圾,最簡(jiǎn)單的同時(shí)也可能是最好的方法是調(diào)用panic函數(shù)來(lái)中斷程序的執(zhí)行以強(qiáng)制發(fā)生錯(cuò)誤哪廓,使得該錯(cuò)誤不會(huì)被忽略,因而能夠被盡快修復(fù)初烘。

姿勢(shì)二:在程序部署后涡真,應(yīng)恢復(fù)異常避免程序終止

在Golang中分俯,某個(gè)Goroutine如果panic了,并且沒(méi)有recover哆料,那么整個(gè)Golang進(jìn)程就會(huì)異常退出缸剪。所以,一旦Golang程序部署后东亦,在任何情況下發(fā)生的異常都不應(yīng)該導(dǎo)致程序異常退出杏节,我們?cè)谏蠈雍瘮?shù)中加一個(gè)延遲執(zhí)行的recover調(diào)用來(lái)達(dá)到這個(gè)目的,并且是否進(jìn)行recover需要根據(jù)環(huán)境變量或配置文件來(lái)定讥此,默認(rèn)需要recover拢锹。 這個(gè)姿勢(shì)類似于C語(yǔ)言中的斷言,但還是有區(qū)別:一般在Release版本中萄喳,斷言被定義為空而失效卒稳,但需要有if校驗(yàn)存在進(jìn)行異常保護(hù),盡管契約式設(shè)計(jì)中不建議這樣做他巨。在Golang中充坑,recover完全可以終止異常展開(kāi)過(guò)程,省時(shí)省力染突。

我們?cè)谡{(diào)用recover的延遲函數(shù)中以最合理的方式響應(yīng)該異常:

  1. 打印堆棧的異常調(diào)用信息和關(guān)鍵的業(yè)務(wù)信息捻爷,以便這些問(wèn)題保留可見(jiàn);
  2. 將異常轉(zhuǎn)換為錯(cuò)誤份企,以便調(diào)用者讓程序恢復(fù)到健康狀態(tài)并繼續(xù)安全運(yùn)行也榄。

一個(gè)簡(jiǎn)單的例子:

func funcA() error {
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("panic recover! p: %v", p)
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}

func test() {
    err := funcA()
    if err == nil {
        fmt.Printf("err is nil\\n")
    } else {
        fmt.Printf("err is %v\\n", err)
    }
}

我們期望test函數(shù)的輸出是:

err is foo

實(shí)際上test函數(shù)的輸出是:

err is nil

原因是panic異常處理機(jī)制不會(huì)自動(dòng)將錯(cuò)誤信息傳遞給error,所以要在funcA函數(shù)中進(jìn)行顯式的傳遞司志,代碼如下所示:

func funcA() (err error) {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}

姿勢(shì)三:對(duì)于不應(yīng)該出現(xiàn)的分支甜紫,使用異常處理

當(dāng)某些不應(yīng)該發(fā)生的場(chǎng)景發(fā)生時(shí),我們就應(yīng)該調(diào)用panic函數(shù)來(lái)觸發(fā)異常骂远。比如囚霸,當(dāng)程序到達(dá)了某條邏輯上不可能到達(dá)的路徑:

switch s := suit(drawCard()); s {
    case "Spades":
    // ...
    case "Hearts":
    // ...
    case "Diamonds":
    // ... 
    case "Clubs":
    // ...
    default:
        panic(fmt.Sprintf("invalid suit %v", s))
}

姿勢(shì)四:針對(duì)入?yún)⒉粦?yīng)該有問(wèn)題的函數(shù),使用panic設(shè)計(jì)

入?yún)⒉粦?yīng)該有問(wèn)題一般指的是硬編碼激才,先看這兩個(gè)函數(shù)(Compile和MustCompile)拓型,其中MustCompile函數(shù)是對(duì)Compile函數(shù)的包裝:

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

所以,對(duì)于同時(shí)支持用戶輸入場(chǎng)景和硬編碼場(chǎng)景的情況瘸恼,一般支持硬編碼場(chǎng)景的函數(shù)是對(duì)支持用戶輸入場(chǎng)景函數(shù)的包裝劣挫。 對(duì)于只支持硬編碼單一場(chǎng)景的情況,函數(shù)設(shè)計(jì)時(shí)直接使用panic钞脂,即返回值類型列表中不會(huì)有error揣云,這使得函數(shù)的調(diào)用處理非常方便(沒(méi)有了乏味的"if err != nil {/ 打印 && 錯(cuò)誤處理 /}"代碼塊)。

錯(cuò)誤封裝的實(shí)踐

用戶自定義類型

重寫的Go里自帶的error類型冰啃,首先從一個(gè)自定義的錯(cuò)誤類型開(kāi)始邓夕,該錯(cuò)誤類型將在程序中識(shí)別為error類型。因此阎毅,引入一個(gè)封裝了Go的 error的新自定義Error類型焚刚。

type GoError struct {
   error
}

上下文數(shù)據(jù)

當(dāng)在Go中說(shuō)error是一個(gè)值時(shí),它是字符串值 - 任何實(shí)現(xiàn)了Error() string函數(shù)的類型都可以視作error類型扇调。將字符串值視為error會(huì)使跨層的error處理復(fù)雜化矿咕,因此處理error字符串信息并不是正確的方法。所以可以使用嵌套錯(cuò)誤把字符串和錯(cuò)誤碼解耦:

type GoError struct {
   error
   Code    string
}

現(xiàn)在對(duì)error的處理將基于錯(cuò)誤碼Code字段而不是字符串狼钮√贾可以通過(guò)上下文數(shù)據(jù)進(jìn)一步對(duì)錯(cuò)誤字符串進(jìn)行解耦,在上下文數(shù)據(jù)中可以使用i18n包進(jìn)行國(guó)際化熬芜。

type GoError struct {
   error
   Code    string
   Data    map[string]interface{}
}

Data包含用于構(gòu)造錯(cuò)誤字符串的上下文數(shù)據(jù)莲镣。錯(cuò)誤字符串可以通過(guò)數(shù)據(jù)模板化:

//i18N def
"InvalidParamValue": "Invalid parameter value '{{.actual}}', expected '{{.expected}}' for '{{.name}}'"

在i18N定義文件中,錯(cuò)誤碼Code將會(huì)映射到使用Data構(gòu)建的模板化的錯(cuò)誤字符串中涎拉。

原因(Causes)

error可能發(fā)生在任何一層瑞侮,有必要為每一層提供處理error的選項(xiàng),并在不丟失原始error值的情況下進(jìn)一步使用附加的上下文信息對(duì)error進(jìn)行包裝鼓拧。GoError結(jié)構(gòu)體可以用Causes進(jìn)一步封裝半火,用來(lái)保存整個(gè)錯(cuò)誤堆棧。

type GoError struct {
   error
   Code    string
   Data    map[string]interface{}
   Causes  []error
}

如果必須保存多個(gè)error數(shù)據(jù)季俩,則causes是一個(gè)數(shù)組類型钮糖,并將其設(shè)置為基本error類型,以便在程序中包含該原因的第三方錯(cuò)誤酌住。

組件(Component)

標(biāo)記層組件將有助于識(shí)別error發(fā)生在哪一層店归,并且可以避免不必要的error wrap。例如赂韵,如果service類型的error組件發(fā)生在服務(wù)層娱节,則可能不需要wrap error。檢查組件信息將有助于防止暴露給用戶不應(yīng)該通知的error祭示,比如數(shù)據(jù)庫(kù)error:

type GoError struct {
   error
   Code      string
   Data      map[string]interface{}
   Causes    []error
   Component ErrComponent
}

type ErrComponent string
const (
   ErrService  ErrComponent = "service"
   ErrRepo     ErrComponent = "repository"
   ErrLib      ErrComponent = "library"
)

響應(yīng)類型(ResponseType)

添加一個(gè)錯(cuò)誤響應(yīng)類型這樣可以支持error分類肄满,以便于了解什么錯(cuò)誤類型。例如质涛,可以根據(jù)響應(yīng)類型(如NotFound)對(duì)error進(jìn)行分類稠歉,像DbRecordNotFoundResourceNotFound汇陆、UserNotFound等等的error都可以歸類為 NotFound error怒炸。這在多層應(yīng)用程序開(kāi)發(fā)過(guò)程中非常有用,而且是可選的封裝:

type GoError struct {
   error
   Code         string
   Data         map[string]interface{}
   Causes       []error
   Component    ErrComponent
   ResponseType ResponseErrType
}

type ResponseErrType string

const (
   BadRequest    ResponseErrType = "BadRequest"
   Forbidden     ResponseErrType = "Forbidden"
   NotFound      ResponseErrType = "NotFound"
   AlreadyExists ResponseErrType = "AlreadyExists"
)

重試

在少數(shù)情況下毡代,出現(xiàn)error會(huì)進(jìn)行重試阅羹。retry字段可以通過(guò)設(shè)置Retryable標(biāo)記來(lái)決定是否要進(jìn)行error重試:

type GoError struct {
   error
   Code         string
   Message      string
   Data         map[string]interface{}
   Causes       []error
   Component    ErrComponent
   ResponseType ResponseErrType
   Retryable    bool
}

GoError 接口

通過(guò)定義一個(gè)帶有GoError實(shí)現(xiàn)的顯式error接口勺疼,可以簡(jiǎn)化error檢查:

package goerr

type Error interface {
   error

   Code() string
   Message() string
   Cause() error
   Causes() []error
   Data() map[string]interface{}
   String() string
   ResponseErrType() ResponseErrType
   SetResponseType(r ResponseErrType) Error
   Component() ErrComponent
   SetComponent(c ErrComponent) Error
   Retryable() bool
   SetRetryable() Error
}

抽象error

有了上述的封裝方式,更重要的是對(duì)error進(jìn)行抽象捏鱼,將這些封裝保存在同一地方执庐,并提供error函數(shù)的可重用性

func ResourceNotFound(id, kind string, cause error) GoError {
   data := map[string]interface{}{"kind": kind, "id": id}
   return GoError{
      Code:         "ResourceNotFound",
      Data:         data,
      Causes:       []error{cause},
      Component:    ErrService,
      ResponseType: NotFound,
      Retryable:    false,
   }
}

這個(gè)error函數(shù)抽象了ResourceNotFound這個(gè)error,開(kāi)發(fā)者可以使用這個(gè)函數(shù)來(lái)返回error對(duì)象而不是每次創(chuàng)建一個(gè)新的對(duì)象:

//UserService
user, err := u.repo.FindUser(ctx, userId)
if err != nil {
   if err.ResponseType == NotFound {
      return ResourceNotFound(userUid, "User", err)
   }
   return err
}

結(jié)論

我們演示了如何使用添加上下文數(shù)據(jù)的自定義Go的error類型导梆,從而使得error在多層應(yīng)用程序中更有意義轨淌。可以在這里[1]看到完整的代碼實(shí)現(xiàn)和定義看尼。

參考資料

[1] 這里: https://gist.github.com/prathabk/744367cbfc70435c56956f650612d64b

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末递鹉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子藏斩,更是在濱河造成了極大的恐慌躏结,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灾茁,死亡現(xiàn)場(chǎng)離奇詭異窜觉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)北专,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門禀挫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拓颓,你說(shuō)我怎么就攤上這事语婴。” “怎么了驶睦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵砰左,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我场航,道長(zhǎng)缠导,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任溉痢,我火速辦了婚禮僻造,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘孩饼。我一直安慰自己髓削,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布镀娶。 她就那樣靜靜地躺著立膛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪梯码。 梳的紋絲不亂的頭發(fā)上宝泵,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天好啰,我揣著相機(jī)與錄音,去河邊找鬼鲁猩。 笑死坎怪,一個(gè)胖子當(dāng)著我的面吹牛罢坝,可吹牛的內(nèi)容都是我干的廓握。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼嘁酿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼隙券!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起闹司,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤娱仔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后游桩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體牲迫,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年借卧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盹憎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铐刘,死狀恐怖陪每,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情镰吵,我是刑警寧澤檩禾,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站疤祭,受9級(jí)特大地震影響盼产,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勺馆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一戏售、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谓传,春花似錦蜈项、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至诗祸,卻和暖如春跑芳,著一層夾襖步出監(jiān)牢的瞬間轴总,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工博个, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怀樟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓盆佣,卻偏偏與公主長(zhǎng)得像往堡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子共耍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容