Go 部落格

錯誤處理與 Go

Andrew Gerrand
2011 年 7 月 12 日

引言

如果您編寫過 Go 程式碼,很可能遇到過內建的 error 型別。Go 程式碼使用 error 值來指示異常狀態。例如,os.Open 函式在開啟檔案失敗時會返回一個非零值的 error

func Open(name string) (file *File, err error)

以下程式碼使用 os.Open 開啟檔案。如果發生錯誤,它會呼叫 log.Fatal 列印錯誤資訊並停止。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

僅瞭解 error 型別的這些資訊,您就可以在 Go 中完成很多工作,但在本文中,我們將更深入地探討 error,並討論 Go 中錯誤處理的一些最佳實踐。

error 型別

error 型別是一個介面型別。一個 error 變數代表任何可以描述自身為字串的值。這是介面的宣告:

type error interface {
    Error() string
}

error 型別,與其他所有內建型別一樣,在 universe block 中是預宣告的

最常用的 error 實現是 errors 包中未匯出的 errorString 型別。

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

您可以使用 errors.New 函式構造其中一個值。它接收一個字串,將其轉換為 errors.errorString 並作為一個 error 值返回。

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

以下是您如何使用 errors.New 的示例

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

呼叫者將負引數傳遞給 Sqrt 時,會收到一個非零值的 error 值(其具體表示是一個 errors.errorString 值)。呼叫者可以透過呼叫 errorError 方法,或者直接列印它來訪問錯誤字串(“math: square root of…”)

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt 包透過呼叫其 Error() string 方法來格式化 error 值。

錯誤實現負責總結上下文。os.Open 返回的錯誤格式為“open /etc/passwd: permission denied”,而不僅僅是“permission denied”。我們的 Sqrt 返回的錯誤缺少有關無效引數的資訊。

要新增該資訊,一個有用的函式是 fmt 包的 Errorf。它根據 Printf 的規則格式化字串,並將其作為由 errors.New 建立的 error 返回。

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

在許多情況下,fmt.Errorf 就足夠好了,但由於 error 是一個介面,您可以使用任意資料結構作為錯誤值,以允許呼叫者檢查錯誤的詳細資訊。

例如,我們假設的呼叫者可能想要恢復傳遞給 Sqrt 的無效引數。我們可以透過定義一個新的錯誤實現而不是使用 errors.errorString 來實現這一點

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

複雜的呼叫者可以使用型別斷言來檢查 NegativeSqrtError 並進行特殊處理,而那些僅將錯誤傳遞給 fmt.Printlnlog.Fatal 的呼叫者將看到行為沒有變化。

另一個例子是,json 包定義了一個 SyntaxError 型別,當 json.Decode 函式在解析 JSON 資料時遇到語法錯誤時會返回此型別。

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

Offset 欄位甚至沒有顯示在錯誤的預設格式中,但呼叫者可以使用它來為他們的錯誤訊息新增檔案和行資訊

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

(這是 Camlistore 專案中一些實際程式碼的一個略微簡化版本。)

error 介面只需要一個 Error 方法;特定的錯誤實現可能具有其他方法。例如,net 包按照慣例返回 error 型別的錯誤,但某些錯誤實現具有 net.Error 介面定義的附加方法

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

客戶端程式碼可以使用型別斷言來測試是否為 net.Error,然後區分瞬時網路錯誤和永久性錯誤。例如,網路爬蟲遇到臨時錯誤時可能會休眠並重試,否則就放棄。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

簡化重複的錯誤處理

在 Go 中,錯誤處理很重要。語言的設計和約定鼓勵您在錯誤發生的地方顯式檢查錯誤(這與其他語言中丟擲異常有時捕獲異常的約定不同)。在某些情況下,這會使 Go 程式碼變得冗長,但幸運的是,您可以使用一些技術來最大限度地減少重複的錯誤處理。

考慮一個 App Engine 應用程式,其中包含一個 HTTP 處理程式,該處理程式從資料儲存中檢索記錄並使用模板對其進行格式化。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

此函式處理 datastore.Get 函式和 viewTemplateExecute 方法返回的錯誤。在這兩種情況下,它都會向用戶顯示帶有 HTTP 狀態碼 500(“內部伺服器錯誤”)的簡單錯誤訊息。這看起來程式碼量 manageable,但新增更多 HTTP 處理程式後,您很快就會發現許多重複的錯誤處理程式碼。

為了減少重複,我們可以定義自己的 HTTP appHandler 型別,該型別包含一個 error 返回值

type appHandler func(http.ResponseWriter, *http.Request) error

然後我們可以修改我們的 viewRecord 函式以返回錯誤

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

這比原始版本更簡單,但 http 包不理解返回 error 的函式。要解決此問題,我們可以在 appHandler 上實現 http.Handler 介面的 ServeHTTP 方法

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

ServeHTTP 方法呼叫 appHandler 函式,並將返回的錯誤(如果有)顯示給使用者。請注意,該方法的接收者 fn 是一個函式。(Go 可以做到!)該方法透過表示式 fn(w, r) 呼叫接收者來呼叫函式。

現在,在向 http 包註冊 viewRecord 時,我們使用 Handle 函式(而不是 HandleFunc),因為 appHandler 是一個 http.Handler(而不是 http.HandlerFunc)。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

有了這個基本的錯誤處理基礎設施,我們可以使其更加使用者友好。與其僅僅顯示錯誤字串,不如給使用者提供帶有適當 HTTP 狀態碼的簡單錯誤訊息,同時將完整的錯誤記錄到 App Engine 開發者控制檯以進行除錯。

為此,我們建立一個包含 error 和其他一些欄位的 appError 結構體

type appError struct {
    Error   error
    Message string
    Code    int
}

接下來我們修改 appHandler 型別以返回 *appError

type appHandler func(http.ResponseWriter, *http.Request) *appError

(通常將錯誤的具體型別而不是 error 傳回是錯誤的,原因在Go FAQ 中有討論,但在這裡是正確的做法,因為 ServeHTTP 是唯一看到該值並使用其內容的地方。)

並使 appHandlerServeHTTP 方法將 appErrorMessage 與正確的 HTTP 狀態碼 Code 一起顯示給使用者,並將完整的 Error 記錄到開發者控制檯

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最後,我們將 viewRecord 更新為新的函式簽名,並在遇到錯誤時讓它返回更多上下文

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

這個版本的 viewRecord 程式碼長度與原始版本相同,但現在每一行都有特定的含義,並且我們提供了更友好的使用者體驗。

改進不止於此;我們可以進一步改進應用程式中的錯誤處理。一些想法包括:

  • 為錯誤處理程式提供一個漂亮的 HTML 模板,

  • 當用戶是管理員時,透過將堆疊跟蹤寫入 HTTP 響應來簡化除錯,

  • appError 編寫一個建構函式,用於儲存堆疊跟蹤以便更輕鬆地除錯,

  • appHandler 內部的 panic 中恢復,將錯誤記錄到控制檯標記為“Critical”,同時告訴使用者“發生了嚴重錯誤”。這是一個很好的細節,可以避免向用戶暴露由程式設計錯誤引起的晦澀難懂的錯誤訊息。有關更多詳細資訊,請參閱 Defer、Panic 和 Recover 文章。

結論

適當的錯誤處理是優秀軟體的基本要求。透過採用本文介紹的技術,您應該能夠編寫更可靠、更簡潔的 Go 程式碼。

下一篇文章:App Engine 上的 Go 現已正式可用
上一篇文章:Go 中的頭等函式
部落格索引