切換語言為:簡體
go error處理方案

go error處理方案

  • 爱糖宝
  • 2024-09-05
  • 2044
  • 0
  • 0

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型別,可以攜帶更多的資訊,返回方增加錯誤資訊時更容易(參考下圖),且能完成錯誤和錯誤值之間的解耦。

go 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的返回形式,而不是盲目跟隨一個建議,選擇一個“合適”的方案,而非唯一的“最佳”方案。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.