在Go中,對于處理錯誤一般分為兩種情況: 錯誤和異常.
在Go中,錯誤的處理一般都是通過 error接口來指定;異常通常都是通過panic來指定。
go的Error
go Error就是一個普通的接口罩抗,普通的值。(https://pkg.go.dev/builtin#error)
type error interface {
Error() string
}
我們經(jīng)常使用 errors.New()來返回一個error對象
分析error包核心代碼
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
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
}
如下面的例子:
package main
func main(){
NotFoundErr := errors.New("沒有找到文件目錄")
}
在go的基礎(chǔ)庫中灿椅,也引用了大量自定義的Error套蒂,例如:
https://go.dev/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")
)
有一個問題钞支,值得思考;為什么errors.New()返回的是內(nèi)部errorString對象的指針呢?
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
因為go比較兩個結(jié)構(gòu)體是否相等時操刀,是依次比較每個字段烁挟,如果不返回結(jié)構(gòu)體指針的話,當字段s相同時就會被認為時同一個錯誤
Error Vs Exception
Go的處理異常邏輯是不引入Exception骨坑,支持多參數(shù)返回撼嗓。
所以我們可以很輕易的在函數(shù)簽名中攜帶實現(xiàn)了error interface的對象,交給調(diào)用者來判定卡啰。
如果一個函數(shù)返回了 value, error静稻,你不能對這個 value 做任何假設(shè),必須先判定 error匈辱。唯一可以忽略 error 的是,如果你連 value 也不關(guān)心杀迹。
其中,Go也有panic機制亡脸,Go panic 意味著 fatal error(就是掛了)。不能假設(shè)調(diào)用者來解決 panic树酪,意味著代碼不能繼續(xù)運行浅碾。
在Go中,使用多個返回值和一個簡單的約定续语,
Go 解決了讓程序員知道什么時候出了問題垂谢,并為真正的異常情況保留了 panic。
對于Panic和error什么時候用比較合適疮茄?
- 在服務(wù)初始化失敗時滥朱,一些main函數(shù)里面的強依賴的基礎(chǔ)組建,例如:mysql/redis/kafka/mq等力试,當連接錯誤時徙邻,必須panic。
- 在代碼中讀取配置文件出錯時畸裳,例如:讀取配置中心出錯缰犁,讀取Apollo出錯,這些基礎(chǔ)配置的讀取怖糊,如果出錯帅容,一定要panic,因為是強依賴伍伤,會影響后續(xù)的程序正常運行并徘。
- 在go語言中,也有異常捕獲(recover)機制, 在對于弱依賴出錯時嚷缭,可以recover.
4 例如不可恢復(fù)的錯誤饮亏,例如:索引越界耍贾,不可恢復(fù)的環(huán)境問題,內(nèi)存溢出路幸,都推薦使用panic來讓程序直接退出荐开。
go的sentinel Error
這個單詞也可以叫:哨兵error.
先說下結(jié)論: 不建議使用sentinel Error
sentinel是什么?
對于預(yù)定義的特定錯誤简肴,我們叫它為 sentinel error (哨兵Error)晃听,這個名字來源于計算機編程中使用一個特定值來表示不可能進行進一步處理的做法。
對于Sentienl Error的例子可以參考 io.EOF,如下圖:
使用 sentinel 值是最不靈活的錯誤處理策略砰识,因為調(diào)用方必須使用 == 將結(jié)果與預(yù)先聲明的值進行比較能扒。當您想要提供更多的上下文時,這就出現(xiàn)了一個問題辫狼,因為返回一個不同的錯誤將破壞相等性檢查初斑。
為什么不推薦使用 sentinel?
- Sentinel errors 會成為你 API 公共部分。
- Sentinel errors 在兩個包之間創(chuàng)建了依賴膨处。
Error Types
Error type 是實現(xiàn)了 error 接口的自定義類型见秤。例如 MyError 類型記錄了文件和行號以展示發(fā)生了什么。
package main
import "fmt"
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d:%s",e.File,e.Line,e.Msg)
}
func test() error {
return &MyError{"something happend","server.go",42}
}
func main() {
err := test()
if err != nil {
fmt.Println(err)
}
}```
```go
因為 MyError 是一個 type真椿,調(diào)用者可以使用斷言轉(zhuǎn)換成這個類型鹃答,來獲取更多的上下文信息。
package main
import "fmt"
type MyError struct {
Msg string
File string
Line int
}
func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d:%s",e.File,e.Line,e.Msg)
}
func test() error {
return &MyError{"something happend","server.go",42}
}
func main() {
err := test()
switch err := err.(type) {
case nil:
fmt.Println("沒有error")
case *MyError:
fmt.Println("error occureed on line:",err.Line)
}
}
與錯誤值相比突硝,錯誤類型的一大改進是它們能夠包裝底層錯誤以提供更多上下文测摔。
一個不錯的例子就是 os.PathError 他提供了底層執(zhí)行了什么操作、那個路徑出了什么問題解恰。
結(jié)論: 調(diào)用者要使用類型斷言和類型 switch锋八,就要讓自定義的 error 變?yōu)?public。這種模型會導(dǎo)致和調(diào)用者產(chǎn)生強耦合修噪,從而導(dǎo)致 API 變得脆弱查库。
結(jié)論是盡量避免使用 error types,雖然錯誤類型比 sentinel errors 更好黄琼,因為它們可以捕獲關(guān)于出錯的更多上下文樊销,但是 error types 共享 error values 許多相同的問題。
因此脏款,我的建議是避免錯誤類型围苫,或者至少避免將它們作為公共 API 的一部分。
Handling Error
- Indented Flow is for errors
無錯誤的正常流程代碼撤师,將成為一條直線剂府,而不是縮進的代碼。
例如:
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff
f,err := os.Open(path)
if err == nil {
// do stuff
}
// handle error
2 Eliminate error handling by eliminating errors
通過消除錯誤來消除錯誤處理
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
// 修改后
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
Wrap errors
通過使用 pkg/errors包剃盾,您可以向錯誤值添加上下文腺占,這種方式即可以由人淤袜,也可以由機器檢查
func Write(w io.Write, buf []byte)error {
_,err := w.Write(buf)
return errors.Wrap(err,"write failed")
}
- 在你的應(yīng)用代碼中,可以使用 errors.New 或者 errors.Errorf 返回錯誤
- 如果和其他庫進行協(xié)作衰伯,考慮使用 errors.Wrap 或者 errors.Wrapf保存堆棧信息铡羡。同樣適用于和標準庫協(xié)作的時候
- 直接返回錯誤,而不是每個地方都到處打日志
在程序的頂部或者工作的goruoutine(請求入口)意鲸,使用 %+v 把堆棧詳情打出來烦周!
go1.13之后的error
go1.13為 errors 和 fmt 標準庫包引入了新特性,以簡化處理包含其他錯誤的錯誤怎顾。其中最重要的是: 包含另一個錯誤的 error 可以實現(xiàn)返回底層錯誤的 Unwrap 方法读慎。如果 e1.Unwrap() 返回 e2,那么我們說 e1 包裝 e2槐雾,您可以展開 e1 以獲得 e2夭委。
按照此約定,我們可以為上面的 QueryError 類型指定一個 Unwrap 方法蚜退,該方法返回其包含的錯誤:
go1.13 errors 包包含兩個用于檢查錯誤的新函數(shù):Is 和 As闰靴。