Go 部落格

錯誤處理與 Go

Andrew Gerrand
2011 年 7 月 12 日

簡介

如果您已經寫過任何 Go 程式碼,您可能已經接觸過內建的 error 類型。Go 程式碼使用 error 值來表示不正常的狀態。例如,當無法開啟檔案時,os.Open 函式會傳回非 nil 的 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 的這些資訊,您就可以完成許多事情,不過在本文中,我們將仔細檢視 error,並討論 Go 中錯誤處理的一些良好做法。

error 類型

error 類型是一種介面類型。一個 error 變數代表任何可以將自己描述為字串的值。以下是這個介面的宣告

type error interface {
    Error() string
}

error 類型,與所有內建的類型一樣,在 預先宣告 宇宙區塊

最常使用的 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: 方根…」)

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

fmt 套件透過呼叫其 error 值的 Error() string 方法來設定格式。

總結內容是 error 執行實作的責任。os.Open 回傳的錯誤會設定成「開啟 /etc/passwd:權限遭拒」,而不是「權限遭拒」。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 是一種介面,您就能使用任意資料結構作為 error 值,讓呼叫者深入檢視 error 細節。

例如,我們 hypothetical 的呼叫者可能會想修復傳遞給 Sqrt 的無效引數。我們能透過定義一個新 error 執行實作,而不是使用 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 套件指定 json.Decode 函式在解析 JSON blob 時遇到語法錯誤時會回傳的 SyntaxError 型別。

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 方法;具體 error 執行實作可能會有多個方法。例如,net 套件回傳 error 型別的 error,其遵循慣例,但某些 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` 函數和 `viewTemplate` 的 `Execute` 方法所傳回的錯誤。在兩個情況中,它都會使用 HTTP 狀態碼 500(「內部伺服器錯誤」)向使用者顯示簡單的錯誤訊息。這看起來像是可管理的程式碼量,但再新增一些 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)` 中呼叫接收器,來呼叫函數。

現在,當使用 `Handle` 函數(而非 `HandleFunc`)註冊 `viewRecord` 至 http 套件時,我們使用 `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` 是唯一一個看到該值並使用其內容的地方。)

並讓 appHandler 的 `ServeHTTP` 方法使用正確的 HTTP 狀態 `Code` 將 `appError` 的 `Message` 顯示給使用者,並將完整的 `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 內部的恐慌復原,將錯誤記錄至主控台為「關鍵」,同時告知使用者「發生了一個嚴重的錯誤」。這是避免讓使用者看到難以理解的由程式設計錯誤所造成的錯誤訊息的好方法。請參閱 延後、恐慌和復原 文章以取得更多詳細資訊。

結論

正確的錯誤處理是好的軟體中不可或缺的要求。透過在本篇文章中說明的技巧,您應該可以寫出更可靠且簡潔的 Go 程式碼。

下一篇:Go for App Engine 現在已經普遍使用
上一篇文章:Go 中的一等函式
部落格索引