Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象、errors.Is 和 errors.As
1. error 接口定義
除用 panic 引發(fā)中斷性錯誤外,還可返回 error 類型錯誤對象來表示函數(shù)調(diào)用狀態(tài)。error 接口是 Go 原生內(nèi)置的類型,它的定義如下:
// $GOROOT/src/builtin/builtin.go
type interface error {Error() string
}
在這個接口類型的聲明中只包含了一個方法 Error 。Error 方法不接受任何參數(shù),但是會返回一個 string 類型的結(jié)果。它的作用是返回錯誤信息的字符串表示形式。
任何實現(xiàn)了 error 的 Error 方法的類型的實例,都可以作為錯誤值賦值給 error 接口變量。
一般情況下在 Go 里只使用 error 類型判斷錯誤, Go 官方希望開發(fā)者能夠很清楚的掌控所有的異常,在每一個可能出現(xiàn)異常的地方都返回或判斷 error 是否存在。
標準庫 errors.New 和 fmt.Errorf 函數(shù)用于創(chuàng)建實現(xiàn) error 接口的錯誤對象。通過判斷錯誤對象實例來確定具體錯誤類型。
err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)
這兩種方法實際上返回的是同一個實現(xiàn)了 error 接口的類型的實例,這個未導出的類型就是errors.errorString,它的定義是這樣的:
// $GOROOT/src/errors/errors.go
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}
2. 創(chuàng)建 error 接口對象
2.1 errors.New 創(chuàng)建 error 接口錯誤對象
在生成 error 類型值的時候,用到了 errors.New 函數(shù)。
這是一種最基本的生成錯誤值的方式。我們調(diào)用它的時候傳入一個由字符串代表的錯誤信息,它會給返回給我們一個包含了這個錯誤信息的 error 類型值。
該值的靜態(tài)類型當然是 error ,而動態(tài)類型則是一個在 errors 包中的,包級私有的類型 *errorString 。
package mainimport "errors"var ErrDivByZero = errors.New("division by zero")func div(x, y int) (int, error) {if y == 0 {return 0, ErrDivByZero}return x / y, nil
}
func main() {switch z, err := div(10, 0); err {case nil:println(z)case ErrDivByZero:panic(err)}
}
輸出結(jié)果:
panic: division by zerogoroutine 1 [running]:
main.main()/home/wohu/gocode/src/hello.go:18 +0xa7
exit status 2
2.2 fmt.Errorf 創(chuàng)建 error 接口錯誤對象
我們已經(jīng)知道,通過調(diào)用 fmt.Printf 函數(shù),并給定占位符 %s 就可以打印出某個值的字符串表示形式。對于其他類型的值來說,只要我們能為這個類型編寫一個 String 方法,就可以自定義它的字符串表示形式。
而對于 error 類型值,它的字符串表示形式則取決于它的 Error 方法。在上述情況下,fmt.Printf 函數(shù)如果發(fā)現(xiàn)被打印的值是一個 error 類型的值,那么就會去調(diào)用它的 Error 方法。fmt 包中的這類打印函數(shù)其實都是這么做的。
順便提一句,當我們想通過模板化的方式生成錯誤信息,并得到錯誤值時,可以使用 fmt.Errorf 函數(shù)。該函數(shù)所做的其實就是先調(diào)用 fmt.Sprintf 函數(shù),得到確切的錯誤信息;再調(diào)用 errors.New 函數(shù),得到包含該錯誤信息的 error 類型值,最后返回該值。
package mainimport ("errors""fmt"
)func main() {err1 := fmt.Errorf("invalid contents: %s", "#$%")err2 := errors.New(fmt.Sprintf("invalid contents: %s", "#$%"))if err1.Error() == err2.Error() {fmt.Println("The error messages in err1 and err2 are the same.")}
}
輸出結(jié)果:
The error messages in err1 and err2 are the same.
fmt.Errorf 函數(shù) 示例:
package mainimport "fmt"func foo(i int, j int) (r int, err error) {if j == 0 {err = fmt.Errorf("參數(shù) 2 不能為 %d", j) //給 err 變量賦值一個 error 對象return //返回 r 和 err,因為定義了返回值變量名,所以不需要在這里寫返回變量}return i / j, err //如果沒有賦值 error 給 err 變量,err 是 nil
}func main() {//傳遞 add 函數(shù)和兩個數(shù)字,計算相加結(jié)果n, err := foo(100, 0)if err != nil { //判斷返回的 err 變量是否為 nil,如果不是,說明函數(shù)調(diào)用出錯,打印錯誤內(nèi)容println(err.Error())} else {println(n)}
}
3. 可導出哨兵錯誤值方式
Go 標準庫采用了定義導出的“哨兵”錯誤值的方式,來輔助錯誤處理方檢視錯誤值并做出錯誤處理分支的決策,比如下面的 bufio 包中定義的“哨兵錯誤”:
// $GOROOT/src/bufio/bufio.go
var (ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")ErrBufferFull = errors.New("bufio: buffer full")ErrNegativeCount = errors.New("bufio: negative count")
)
下面的代碼片段利用了上面的哨兵錯誤,進行錯誤處理分支的決策:
data, err := b.Peek(1)
if err != nil {switch err {case bufio.ErrNegativeCount:// ... ...returncase bufio.ErrBufferFull:// ... ...returncase bufio.ErrInvalidUnreadByte:// ... ...returndefault:// ... ...return}
}
一般“哨兵”錯誤值變量以 ErrXXX 格式命名。
從 Go 1.13 版本開始,標準庫 errors 包提供了 Is 函數(shù)用于錯誤處理方對錯誤值的檢視。Is 函數(shù)類似于把一個 error 類型變量與“哨兵”錯誤值進行比較,比如下面代碼:
// 類似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {// 越界的錯誤處理
}
不同的是,如果 error 類型變量的底層錯誤值是一個包裝錯誤(Wrapped Error),errors.Is 方法會沿著該包裝錯誤所在錯誤鏈(Error Chain),與鏈上所有被包裝的錯誤(Wrapped Error)進行比較,直至找到一個匹配的錯誤為止。下面是 Is 函數(shù)應用的一個例子:
var ErrSentinel = errors.New("the underlying sentinel error")func main() {err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)err2 := fmt.Errorf("wrap err1: %w", err1)println(err2 == ErrSentinel) //falseif errors.Is(err2, ErrSentinel) {println("err2 is ErrSentinel")return}println("err2 is not ErrSentinel")
}
在這個例子中,我們通過 fmt.Errorf 函數(shù),并且使用 %w 創(chuàng)建包裝錯誤變量 err1 和 err2,其中 err1 實現(xiàn)了對 ErrSentinel 這個“哨兵錯誤值”的包裝,而 err2 又對 err1 進行了包裝,這樣就形成了一條錯誤鏈。位于錯誤鏈最上層的是 err2,位于最底層的是 ErrSentinel。之后,我們再分別通過值比較和 errors.Is這兩種方法,判斷 err2 與 ErrSentinel 的關系。運行上述代碼,我們會看到如下結(jié)果:
false
err2 is ErrSentinel
我們看到,通過比較操作符對 err2 與 ErrSentinel 進行比較后,我們發(fā)現(xiàn)這二者并不相同。而 errors.Is 函數(shù)則會沿著 err2 所在錯誤鏈,向下找到被包裝到最底層的“哨兵”錯誤值 ErrSentinel。
所以,如果你使用的是 Go 1.13 及后續(xù)版本,我建議你盡量使用 errors.Is 方法去檢視某個錯誤值是否就是某個預期錯誤值,或者包裝了某個特定的“哨兵”錯誤值。
4. 錯誤值類型檢視策略
上面基于 Go 標準庫提供的錯誤值構(gòu)造方法構(gòu)造的“哨兵”錯誤值,除了讓錯誤處理方可以“有的放矢”的進行值比較之外,并沒有提供其他有效的錯誤上下文信息。那如果遇到錯誤處理方需要錯誤值提供更多的“錯誤上下文”的情況,上面這些錯誤處理策略和錯誤值構(gòu)造方式都無法滿足。
這種情況下,我們需要通過自定義錯誤類型的構(gòu)造錯誤值的方式,來提供更多的“錯誤上下文”信息。并且,由于錯誤值都通過 error 接口變量統(tǒng)一呈現(xiàn),要得到底層錯誤類型攜帶的錯誤上下文信息,錯誤處理方需要使用 Go 提供的類型斷言機制(Type Assertion)或類型選擇機制(Type Switch),這種錯誤處理方式,我稱之為錯誤值類型檢視策略。
從 Go 1.13 版本開始,標準庫 errors 包提供了As 函數(shù)給錯誤處理方檢視錯誤值。As 函數(shù)類似于通過類型斷言判斷一個 error 類型變量是否為特定的自定義錯誤類型,如下面代碼所示:
// 類似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {// 如果err類型為*MyError,變量e將被設置為對應的錯誤值
}
不同的是,如果 error 類型變量的動態(tài)錯誤值是一個包裝錯誤,errors.As 函數(shù)會沿著該包裝錯誤所在錯誤鏈,與鏈上所有被包裝的錯誤的類型進行比較,直至找到一個匹配的錯誤類型,就像 errors.Is 函數(shù)那樣。下面是 As 函數(shù)應用的一個例子:
type MyError struct {e string
}func (e *MyError) Error() string {return e.e
}func main() {var err = &MyError{"MyError error demo"}err1 := fmt.Errorf("wrap err: %w", err)err2 := fmt.Errorf("wrap err1: %w", err1)var e *MyErrorif errors.As(err2, &e) {println("MyError is on the chain of err2")println(e == err) return } println("MyError is not on the chain of err2")
}
運行上述代碼會得到:
MyError is on the chain of err2
true
我們看到,errors.As 函數(shù)沿著 err2 所在錯誤鏈向下找到了被包裝到最深處的錯誤值,并將 err2 與其類型 * MyError 成功匹配。匹配成功后,errors.As 會將匹配到的錯誤值存儲到 As 函數(shù)的第二個參數(shù)中,這也是為什么 println(e == err) 輸出 true 的原因。
所以,如果你使用的是 Go 1.13 及后續(xù)版本,請盡量使用 errors.As 方法去檢視某個錯誤值是否是某自定義錯誤類型的實例。
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象、errors.Is 和 errors.As的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022-2028年中国三轴陀螺仪行业市
- 下一篇: 2022-2028中国空中互联网系统市场