error是什麼
error是go原生內建的介面型別,用於表示錯誤,本質上與其他介面型別並無區別。在go的設計哲學中,開發人員需要顯式關注和處理error。開發過程中,要時刻關注error值,並進行處理,以提高系統健壯性。其定義如下:
type interface error { Error() string }
在go中,只要實現了Error() string 這個方法,就可以作為錯誤賦值error介面變數。 目前在go中主要提供了兩種生成error變數的方法;分別如下
fmt.Errorf() errors.New()
fmt.Errorf() 返回的是一個包裝的錯誤資訊,能夠返回錯誤鏈。
errors.New() 返回的錯誤資訊較為簡單,僅包含錯誤訊息的字串。
一般作為約定,在方法或者函式中返回error時,會將error置於返回值的末尾,若沒有錯誤時,則直接返回nil即可。
error使用
軟體設計,各種準則與設計模式都是圍繞著設計出“低耦合”架構而努力,也即降低模組,類,方法等之間的耦合程度。我們應該在使用設計error的過程中,也應該朝著降低呼叫方與實現方之間的耦合度方向進行設計。 為此,我們在使用的過程中,可以以下方面考慮設計考慮。
準則
首先我們可以先考慮從以下準則進行思考改進:
呼叫方是否關心錯誤內容
區分預期錯誤和非預期錯誤,並預先定義預期錯誤
使用fmt.Errorf() 包裝錯誤。
使用errors.Is和 errors.As比較錯誤
自定義錯誤,包裝複雜資訊
error返回
錯誤的返回,由實現者處理,實現者需要根據呼叫方的需求,選擇不同的返回策略。
直接返回:呼叫方不關心錯誤值,遇到錯誤時,就會進入唯一的處理路徑。這種情況下,error透過errors.New或fmt.Errorf() 返回即可,無需做太多設計。這種模式下,雙方之間的耦合最低,互相之間不需要感知error的具體值,只需要感知有錯誤發生即可。一般的程式碼結構如下:
func f1() error { ... return errors.New("some errors") } err := f1() //呼叫方不關心err的具體內容,只要是error就處理。 if err != nil { ... return err }
定義預期錯誤:呼叫方關心錯誤值,根據錯誤值進入不同的處理路徑。此時,實現方可提前定義好預期錯誤並返回,暴露給呼叫方使用。此時,雙方對錯誤值產生了耦合,實現方透過提前定義預期錯誤,降低修改錯誤值可能造成的影響。一般程式碼結構如下:
// 提前定義預期錯誤 var ( Err1 = errors.New("err1...") Err2 = errors.New("err2...") ) //呼叫方關注錯誤 switch err { Err1: ... Err2: ... }
一般約定預期錯誤定義格式為ErrXXX
自定義錯誤:透過自定義錯誤,方便錯誤攜帶更多的資訊,還可以解耦錯誤值和錯誤型別,使用方可以利用錯誤型別執行不同的業務邏輯,而無需關注錯誤值。如go(1.21版本)中定義了OpError
type OpError struct { Op string Net string ... }
自定義error型別,可以攜帶更多的資訊,返回方增加錯誤資訊時更容易(參考下圖),且能完成錯誤和錯誤值之間的解耦。
對於自定義錯誤的設計,我們可以從以下幾個方面思考:
實現者不需要做額外的工作,只需要專注於處理包自身的錯誤
呼叫者知道發生了什麼錯誤
呼叫者能夠決定如何處理這個錯誤
呼叫者能夠了解為什麼傳送這個錯誤
error比較
直接比較;直接比較常用,一般就是 if err != nil 或者 if err == Err1 這樣,直接比較目前還是主流方式,能覆蓋大部分使用場景。
errors.Is : 主要用於比較錯誤值,類似於 if err == Err1,與其不同的是,如果比較返回的error是一個包裝型別時,errors.Is會在錯誤鏈上與所有的包裝錯誤進行比較,直至最後找到一個匹配值。如:
type Err struct { } func (e *Err) Error() string { return "Err!" } func main() { e := &Err{} e1 := fmt.Errorf("Err1: %w", e) e2 := fmt.Errorf("Err2: %w", e1) fmt.Println(errors.Is(e2, e)) }
示例中error.Is逐層值比較,最後找到e2中與e的匹配error,最後列印true;
errors.As:類似於error的型別斷言 if e,ok := err.(Err1) ; 不同是會在錯誤鏈上,將所有的包裝錯誤型別進行比較,並將第一個型別匹配的error,賦值到第二個引數值。如:
type Err struct { e string } func (e *Err) Error() string { return e.e } func main() { e := &Err{"my err1"} e1 := fmt.Errorf("Err1: %w", e) e2 := fmt.Errorf("Err2: %w", e1) var e3 *Err fmt.Println(errors.As(e2, &e3)) fmt.Println(e == e3) }
示例中errors.As逐層進行型別匹配,最後匹配到與e3型別匹配的e,並將e賦值給e3。最後會列印兩個true。
思考
go程式設計中,關於error處理,常聽到 “Make your errors clear with fmt.Errorf, don not just leave them bare.” 的建議,鼓勵使用fmt.Errorf返回error,而非errors.New。
作為使用者,我們應該做的是根據實際需要來決定的error的返回形式,而不是盲目跟隨一個建議,選擇一個“合適”的方案,而非唯一的“最佳”方案。