在實(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ù)panic
和recover
來(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:
- 內(nèi)建函數(shù)
- 假如函數(shù)F中書(shū)寫了panic語(yǔ)句纬纪,會(huì)終止其后要執(zhí)行的代碼,在panic所在函數(shù)F內(nèi)如果存在要執(zhí)行的defer函數(shù)列表滑肉,按照defer的逆序執(zhí)行
- 返回函數(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
- 直到goroutine整個(gè)退出六荒,并報(bào)告錯(cuò)誤
recover:
- 內(nèi)建函數(shù)
- 用來(lái)控制一個(gè)goroutine的panic行為护姆,捕獲panic,從而影響應(yīng)用的行為
- 一般的調(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)換的:
- 錯(cuò)誤轉(zhuǎn)異常坦刀,比如程序邏輯上嘗試請(qǐng)求某個(gè)URL愧沟,最多嘗試三次,嘗試三次的過(guò)程中請(qǐng)求失敗是錯(cuò)誤鲤遥,嘗試完第三次還不成功的話沐寺,失敗就被提升為異常了。
- 異常轉(zhuǎn)錯(cuò)誤盖奈,比如panic觸發(fā)的異常被recover恢復(fù)后混坞,將返回值中error類型的變量進(jìn)行賦值,以便上層函數(shù)繼續(xù)走錯(cuò)誤處理流程。
什么情況下用錯(cuò)誤表達(dá)究孕,什么情況下用異常表達(dá)啥酱,就得有一套規(guī)則,否則很容易出現(xiàn)一切皆錯(cuò)誤或一切皆異常的情況厨诸。
以下給出異常處理的作用域(場(chǎng)景):
- 空指針引用
- 下標(biāo)越界
- 除數(shù)為0
- 不應(yīng)該出現(xiàn)的分支镶殷,比如default
- 輸入不應(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可能為:
- "record is not existed."
- "record is not exist!"
- "record is not existed!!明肮!"
- ...
這使得相同的錯(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è)案例:
- 我們平時(shí)上網(wǎng)時(shí),嘗試請(qǐng)求某個(gè)URL,有時(shí)第一次沒(méi)有響應(yīng)犯祠,當(dāng)我們?cè)俅嗡⑿聲r(shí)法严,就有了驚喜俊柔。
- 團(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)該異常:
- 打印堆棧的異常調(diào)用信息和關(guān)鍵的業(yè)務(wù)信息捻爷,以便這些問(wèn)題保留可見(jiàn);
- 將異常轉(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)行分類稠歉,像DbRecordNotFound
、ResourceNotFound
汇陆、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