詳解golang函數(shù)多返回值錯誤處理與error類型
一、error 類型與錯誤值構(gòu)造
1.1 Error 接口介紹
在Go語言中,error
類型是一個接口類型,通常用于表示錯誤。它定義如下:
type error interface { Error() string }
error
接口只有一個方法,即 Error()
方法,該方法返回一個描述錯誤的字符串。這意味著任何實現(xiàn)了 Error()
方法的類型都可以被用作錯誤類型。通常,Go程序中的函數(shù)在遇到錯誤時會返回一個 error
類型的值,以便調(diào)用方可以處理或記錄錯誤信息。
1.2 構(gòu)造錯誤值的方法
1.2.1 使用errors包
Go 語言的設(shè)計者提供了兩種方便 Go
開發(fā)者構(gòu)造錯誤值的方法: errors.New
和 fmt.Errorf
。
errors.New()
函數(shù)是創(chuàng)建最簡單的錯誤值的方法,它只包含一個錯誤消息字符串。這個方法適用于創(chuàng)建簡單的錯誤值。fmt.Errorf()
函數(shù)允許你構(gòu)造一個格式化的錯誤消息,類似于fmt.Printf()
函數(shù)。這對于需要構(gòu)建更復雜的錯誤消息時非常有用。
使用這兩種方法,我們可以輕松構(gòu)造出一個滿足 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 }
大多數(shù)情況下,使用這兩種方法構(gòu)建的錯誤值就可以滿足我們的需求了。但我們也要看到,雖然這兩種構(gòu)建錯誤值的方法很方便,但它們給錯誤處理者提供的錯誤上下文(Error Context)只限于以字符串形式呈現(xiàn)的信息,也就是 Error 方法返回的信息。
1.2.2 自定義錯誤類型
在一些場景下,錯誤處理者需要從錯誤值中提取出更多信息,幫助他選擇錯誤處理路徑,顯然這兩種方法就不能滿足了。這個時候,我們可以自定義錯誤類型來滿足這一需求。以下是一個示例:
package main import "fmt" // 自定義錯誤類型 type MyError struct { ErrorCode int ErrorMessage string } // 實現(xiàn) error 接口的 Error 方法 func (e MyError) Error() string { return fmt.Sprintf("錯誤 %d: %s", e.ErrorCode, e.ErrorMessage) } func someFunction() error { // 創(chuàng)建自定義錯誤值 err := MyError{ ErrorCode: 404, ErrorMessage: "未找到", } return err } func main() { // 調(diào)用 someFunction,返回自定義錯誤值 err := someFunction() // 打印錯誤信息 fmt.Println("錯誤:", err) }
我們再來看一個例子,比如:標準庫中的 net
包就定義了一種攜帶額外錯誤上下文的錯誤類型:
// $GOROOT/src/net/net.go type OpError struct { Op string Net string Source Addr Addr Addr Err error }
這樣,錯誤處理者就可以根據(jù)這個類型的錯誤值提供的額外上下文信息,比如 Op、Net、Source 等,做出錯誤處理路徑的選擇,比如下面標準庫中的代碼:
// $GOROOT/src/net/http/server.go func isCommonNetReadError(err error) bool { if err == io.EOF { return true } if neterr, ok := err.(net.Error); ok && neterr.Timeout() { return true } if oe, ok := err.(*net.OpError); ok && oe.Op == "read" { return true } return false }
我們看到,上面這段代碼利用類型斷言(Type Assertion
),判斷 error
類型變量 err 的動態(tài)類型是否為 *net.OpError
或 net.Error。如果 err
的動態(tài)類型是 *net.OpError
,那么類型斷言就會返回這個動態(tài)類型的值(存儲在 oe
中),代碼就可以通過判斷它的 Op
字段是否為"read
"來判斷它是否為 CommonNetRead
類型的錯誤。
二、error 類型的好處
2.1 第一點:統(tǒng)一了錯誤類型
如果不同開發(fā)者的代碼、不同項目中的代碼,甚至標準庫中的代碼,都統(tǒng)一以 error
接口變量的形式呈現(xiàn)錯誤類型,就能在提升代碼可讀性的同時,還更容易形成統(tǒng)一的錯誤處理策略。
2.2 第二點:錯誤是值
我們構(gòu)造的錯誤都是值,也就是說,即便賦值給 error 這個接口類型變量,我們也可以像整型值那樣對錯誤做“==”和“!=”的邏輯比較,函數(shù)調(diào)用者檢視錯誤時的體驗保持不變。
由于 error 是一個接口類型,默認零值為nil
。所以我們通常將調(diào)用函數(shù)返回的錯誤與nil
進行比較,以此來判斷函數(shù)是否返回錯誤。如果返回的錯誤為 nil
,則表示函數(shù)執(zhí)行成功,否則表示出現(xiàn)了錯誤。這種約定使得錯誤處理變得一致和直觀。例如你會經(jīng)常看到類似下面的錯誤判斷代碼。
func someFunction() error { // 模擬一個出錯的情況 return errors.New("這是一個錯誤") } func main() { err := someFunction() if err != nil { fmt.Println("函數(shù)執(zhí)行失敗,錯誤信息:", err) } else { fmt.Println("函數(shù)執(zhí)行成功") } }
2.3 第三點:易擴展,支持自定義錯誤上下文
雖然錯誤以 error 接口變量的形式統(tǒng)一呈現(xiàn),但我們很容易通過自定義錯誤類型來擴展我們的錯誤上下文,就像前面的 Go 標準庫的 OpError
類型那樣。
error 接口是錯誤值的提供者與錯誤值的檢視者之間的契約。error 接口的實現(xiàn)者負責提供錯誤上下文,供負責錯誤處理的代碼使用。這種錯誤具體上下文與作為錯誤值類型的 error 接口類型的解耦,也體現(xiàn)了 Go 組合設(shè)計哲學中“正交”的理念。
三、Go 錯誤處理的慣用策略
3.1 策略一:透明錯誤處理策略
簡單來說,Go 語言中的錯誤處理,就是根據(jù)函數(shù) / 方法返回的 error
類型變量中攜帶的錯誤值信息做決策,并選擇后續(xù)代碼執(zhí)行路徑的過程。
這樣,最簡單的錯誤策略莫過于完全不關(guān)心返回錯誤值攜帶的具體上下文信息,只要發(fā)生錯誤就進入唯一的錯誤處理執(zhí)行路徑,比如下面這段代碼:
err := doSomething() if err != nil { // 不關(guān)心err變量底層錯誤值所攜帶的具體上下文信息 // 執(zhí)行簡單錯誤處理邏輯并返回 ... ... return err }
這是 Go 語言中最常見的錯誤處理策略,80% 以上的 Go 錯誤處理情形都可以歸類到這種策略下。在這種策略下,由于錯誤處理方并不關(guān)心錯誤值的上下文,所以錯誤值的構(gòu)造方(如上面的函數(shù) doSomething)可以直接使用 Go 標準庫提供的兩個基本錯誤值構(gòu)造方法 errors.New 和 fmt.Errorf 來構(gòu)造錯誤值,就像下面這樣:
func doSomething(...) error { ... ... return errors.New("some error occurred") }
這樣構(gòu)造出的錯誤值代表的上下文信息,對錯誤處理方是透明的,因此這種策略稱為“透明錯誤處理策略”。在錯誤處理方不關(guān)心錯誤值上下文的前提下,透明錯誤處理策略能最大程度地減少錯誤處理方與錯誤值構(gòu)造方之間的耦合關(guān)系。
3.2 策略二:“哨兵”錯誤處理策略
當錯誤處理方不能只根據(jù)“透明的錯誤值”就做出錯誤處理路徑選取的情況下,錯誤處理方會嘗試對返回的錯誤值進行檢視,于是就有可能出現(xiàn)下面代碼中的反模式:
data, err := b.Peek(1) if err != nil { switch err.Error() { case "bufio: negative count": // ... ... return case "bufio: buffer full": // ... ... return case "bufio: invalid use of UnreadByte": // ... ... return default: // ... ... return } }
簡單來說,反模式就是,錯誤處理方以透明錯誤值所能提供的唯一上下文信息(描述錯誤的字符串),作為錯誤處理路徑選擇的依據(jù)。但這種“反模式”會造成嚴重的隱式耦合。這也就意味著,錯誤值構(gòu)造方不經(jīng)意間的一次錯誤描述字符串的改動,都會造成錯誤處理方處理行為的變化,并且這種通過字符串比較的方式,對錯誤值進行檢視的性能也很差。
那這有什么辦法嗎?Go 標準庫采用了定義導出的(Exported)“哨兵”錯誤值的方式,來輔助錯誤處理方檢視(inspect)錯誤值并做出錯誤處理分支的決策,比如下面的 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: // ... ... return case bufio.ErrBufferFull: // ... ... return case bufio.ErrInvalidUnreadByte: // ... ... return default: // ... ... return } }
你可以看到,一般“哨兵”錯誤值變量以 ErrXXX 格式命名。和透明錯誤策略相比,“哨兵”策略讓錯誤處理方在有檢視錯誤值的需求時候,可以“有的放矢”。
不過,對于 API 的開發(fā)者而言,暴露“哨兵”錯誤值也意味著這些錯誤值和包的公共函數(shù) / 方法一起成為了 API 的一部分。一旦發(fā)布出去,開發(fā)者就要對它進行很好的維護。而“哨兵”錯誤值也讓使用這些值的錯誤處理方對它產(chǎn)生了依賴。
從 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ù)應(yīng)用的一個例子:
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) //false if 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 的關(guān)系。運行上述代碼,我們會看到如下結(jié)果:
false
err2 is ErrSentinel
我們看到,通過比較操作符對 err2 與 ErrSentinel 進行比較后,我們發(fā)現(xiàn)這二者并不相同。而 errors.Is 函數(shù)則會沿著 err2 所在錯誤鏈,向下找到被包裝到最底層的“哨兵”錯誤值ErrSentinel
。
如果你使用的是 Go 1.13 及后續(xù)版本,建議你盡量使用errors.Is
方法去檢視某個錯誤值是否就是某個預(yù)期錯誤值,或者包裝了某個特定的“哨兵”錯誤值。
3.3 策略三:錯誤值類型檢視策略
上面我們看到,基于 Go 標準庫提供的錯誤值構(gòu)造方法構(gòu)造的“哨兵”錯誤值,除了讓錯誤處理方可以“有的放矢”的進行值比較之外,并沒有提供其他有效的錯誤上下文信息。那如果遇到錯誤處理方需要錯誤值提供更多的“錯誤上下文”的情況,上面這些錯誤處理策略和錯誤值構(gòu)造方式都無法滿足。
這種情況下,我們需要通過自定義錯誤類型的構(gòu)造錯誤值的方式,來提供更多的“錯誤上下文”信息。并且,由于錯誤值都通過 error 接口變量統(tǒng)一呈現(xiàn),要得到底層錯誤類型攜帶的錯誤上下文信息,錯誤處理方需要使用 Go 提供的類型斷言機制(Type Assertion)或類型選擇機制(Type Switch),這種錯誤處理方式,我稱之為錯誤值類型檢視策略。
我們來看一個標準庫中的例子加深下理解,這個 json 包中自定義了一個 UnmarshalTypeError
的錯誤類型:
// $GOROOT/src/encoding/json/decode.go type UnmarshalTypeError struct { Value string Type reflect.Type Offset int64 Struct string Field string }
錯誤處理方可以通過錯誤類型檢視策略,獲得更多錯誤值的錯誤上下文信息,下面就是利用這一策略的json
包的一個方法的實現(xiàn):
// $GOROOT/src/encoding/json/decode.go func (d *decodeState) addErrorContext(err error) error { if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 { switch err := err.(type) { case *UnmarshalTypeError: err.Struct = d.errorContext.Struct.Name() err.Field = strings.Join(d.errorContext.FieldStack, ".") return err } } return err }
我們看到,這段代碼通過類型 switch 語句得到了 err 變量代表的動態(tài)類型和值,然后在匹配的 case 分支中利用錯誤上下文信息進行處理。
這里,一般自定義導出的錯誤類型以 XXXError
的形式命名。和“哨兵”錯誤處理策略一樣,錯誤值類型檢視策略,由于暴露了自定義的錯誤類型給錯誤處理方,因此這些錯誤類型也和包的公共函數(shù) / 方法一起,成為了 API 的一部分。一旦發(fā)布出去,開發(fā)者就要對它們進行很好的維護。而它們也讓使用這些類型進行檢視的錯誤處理方對其產(chǎn)生了依賴。
從 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將被設(shè)置為對應(yīng)的錯誤值 }
不同的是,如果 error 類型變量的動態(tài)錯誤值是一個包裝錯誤,errors.As
函數(shù)會沿著該包裝錯誤所在錯誤鏈,與鏈上所有被包裝的錯誤的類型進行比較,直至找到一個匹配的錯誤類型,就像 errors.Is
函數(shù)那樣。下面是As
函數(shù)應(yīng)用的一個例子:
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 *MyError if 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方法去檢視某個錯誤值是否是某自定義錯誤類型的實例。
3.4 策略四:錯誤行為特征檢視策略
不知道你注意到?jīng)]有,在前面我們已經(jīng)講過的三種策略中,其實只有第一種策略,也就是“透明錯誤處理策略”,有效降低了錯誤的構(gòu)造方與錯誤處理方兩者之間的耦合。雖然前面的策略二和策略三,都是我們實際編碼中有效的錯誤處理策略,但其實使用這兩種策略的代碼,依然在錯誤的構(gòu)造方與錯誤處理方兩者之間建立了耦合。
那么除了“透明錯誤處理策略”外,我們是否還有手段可以降低錯誤處理方與錯誤值構(gòu)造方的耦合呢?
在 Go 標準庫中,我們發(fā)現(xiàn)了這樣一種錯誤處理方式:將某個包中的錯誤類型歸類,統(tǒng)一提取出一些公共的錯誤行為特征,并將這些錯誤行為特征放入一個公開的接口類型中。這種方式也被叫做錯誤行為特征檢視策略。
以標準庫中的net
包為例,它將包內(nèi)的所有錯誤類型的公共行為特征抽象并放入 net.Error
這個接口中,如下面代碼:
// $GOROOT/src/net/net.go type Error interface { error Timeout() bool Temporary() bool }
我們看到,net.Error
接口包含兩個用于判斷錯誤行為特征的方法:Timeout
用來判斷是否是超時(Timeout
)錯誤,Temporary
用于判斷是否是臨時(Temporary
)錯誤。
而錯誤處理方只需要依賴這個公共接口,就可以檢視具體錯誤值的錯誤行為特征信息,并根據(jù)這些信息做出后續(xù)錯誤處理分支選擇的決策。
這里,我們再看一個 http 包使用錯誤行為特征檢視策略進行錯誤處理的例子,加深下理解:
// $GOROOT/src/net/http/server.go func (srv *Server) Serve(l net.Listener) error { ... ... for { rw, e := l.Accept() if e != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := e.(net.Error); ok && ne.Temporary() { // 注:這里對臨時性(temporary)錯誤進行處理 ... ... time.Sleep(tempDelay) continue } return e } ... } ... ... }
在上面代碼中,Accept
方法實際上返回的錯誤類型為 *OpError,它是 net
包中的一個自定義錯誤類型,它實現(xiàn)了錯誤公共特征接口 net.Error
,如下代碼所示:
// $GOROOT/src/net/net.go type OpError struct { ... ... // Err is the error that occurred during the operation. Err error } type temporary interface { Temporary() bool } func (e *OpError) Temporary() bool { if ne, ok := e.Err.(*os.SyscallError); ok { t, ok := ne.Err.(temporary) return ok && t.Temporary() } t, ok := e.Err.(temporary) return ok && t.Temporary() }
因此,OpError 實例可以被錯誤處理方通過 net.Error 接口的方法,判斷它的行為是否滿足 Temporary 或 Timeout 特征。
四、總結(jié)
Go 語言統(tǒng)一錯誤類型為 error 接口類型,并提供了多種快速構(gòu)建可賦值給 error 類型的錯誤值的函數(shù),包括 errors.New、fmt.Errorf 等,我們還講解了使用統(tǒng)一 error 作為錯誤類型的優(yōu)點,你要深刻理解這一點。
基于 Go 錯誤處理機制、統(tǒng)一的錯誤值類型以及錯誤值構(gòu)造方法的基礎(chǔ)上,Go 語言形成了多種錯誤處理的慣用策略,包括透明錯誤處理策略、“哨兵”錯誤處理策略、錯誤值類型檢視策略以及錯誤行為特征檢視策略等。這些策略都有適用的場合,但沒有某種單一的錯誤處理策略可以適合所有項目或所有場合。
在錯誤處理策略選擇上,你可以參考以下:
- 請盡量使用“透明錯誤”處理策略,降低錯誤處理方與錯誤值構(gòu)造方之間的耦合;
- 如果可以從眾多錯誤類型中提取公共的錯誤行為特征,那么請盡量使用“錯誤行為特征檢視策略”;
- 在上述兩種策略無法實施的情況下,再使用“哨兵”策略和“錯誤值類型檢視”策略;
- Go 1.13 及后續(xù)版本中,盡量用
errors.Is
和errors.As
函數(shù)替換原先的錯誤檢視比較語句。
以上就是詳解golang函數(shù)多返回值錯誤處理與error類型的詳細內(nèi)容,更多關(guān)于golang錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解golang中?work與?module?的區(qū)別與聯(lián)系
Go?模塊通常由一個項目或庫組成,并包含一組隨后一起發(fā)布的?Go?包,Go?模塊通過允許用戶將項目代碼放在他們選擇的目錄中并為每個模塊指定依賴項的版本,解決了原始系統(tǒng)的許多問題,本文將給大家介紹一下golang中?work與?module?的區(qū)別與聯(lián)系,需要的朋友可以參考下2023-09-09利用Go語言實現(xiàn)輕量級OpenLdap弱密碼檢測工具
這篇文章主要為大家詳細介紹了如何利用Go語言實現(xiàn)輕量級OpenLdap弱密碼檢測工具,文中的示例代碼講解詳細,感興趣的小伙伴可以嘗試一下2022-09-09Go Grpc Gateway兼容HTTP協(xié)議文檔自動生成網(wǎng)關(guān)
這篇文章主要為大家介紹了Go Grpc Gateway兼容HTTP協(xié)議文檔自動生成網(wǎng)關(guān)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06Go語言構(gòu)建流數(shù)據(jù)pipeline的示例詳解
Go的并發(fā)原語可以輕松構(gòu)建流數(shù)據(jù)管道,從而高效利用?I/O?和多個?CPU,?本文展示了此類pipelines的示例,強調(diào)了操作失敗時出現(xiàn)的細微之處,并介紹了干凈地處理失敗的技術(shù),希望對大家有所幫助2024-02-02golang中結(jié)構(gòu)體嵌套接口的實現(xiàn)
本文主要介紹了golang中結(jié)構(gòu)體嵌套接口的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04