[譯]go錯誤處理

原文來自Error handling and Gogit

背景介紹

若是你有寫過Go代碼,那麼你能夠會遇到Go中內建類型error。Go語言使用error*值來顯示異常狀態。例如,os.Open在打開文件錯誤時,會返回一個非nil error值。github

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

下面的代碼使用os.Open來打開一個文件。若是出現錯誤,會調用log.Fatal打印出錯誤的信息而且終止代碼。golang

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

在使用Go的工做中,上面的例子已經能知足大多數狀況,可是這篇文章會更進一步的探討關於捕獲異常的實踐。數據庫

error類型

error類型是一個interface類型。一個error變量能夠經過任何能夠描述本身的string類型的值來展現本身。下面是它的接口描述:json

type error interface {
    Error() String
}

error類型,就像其餘內建類型同樣,==是在全局中預先聲明的==。這意味着咱們不用導入就能夠在任何地方使用它。網絡

最經常使用的error實現是在 errors 包中定義的一個不可導出的類型:errorStringapp

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

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

經過errors.New函數能夠建立一個errorString實例.該函數接收一個string參數,並將string參數轉換爲一個erros.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, error.New("math: squara root of negative number")
    }
    // implementation
}

在調用Sqrt時,若是傳入的參數是負數,調用者會接收到Sqrt返回的一個非空error值(正確來講應該是一個errors.errorString值)。調用者能夠經過調用errorError方法或者經過打印來獲得錯誤信息字段("math: squara root of nagative number")。code

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

fmt包經過調用Error()方法來格式化error

一個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是一個接口,所以你能夠定義任意的數據類型來做爲error值,以供調用者獲取更多的錯誤細節。

例如,若是有一個比較複雜的調用者想要恢復傳給Sqrt的非法參數。咱們經過定義一個新的錯誤實現而不是使用errors.errorString來實現這個需求:

type NegativeSqrtError float64

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

一個複雜的調用者就可使用類型斷言(type assertion)來檢測NegativeSqrtError而且捕獲它,與此同時,對於使用fmt.Println或者log.Fatal來輸出錯誤的方式來講卻沒有改變他們的行爲。

另外一個例子來自json包,當咱們在使用json.Decode函數時,若是咱們傳入了一個不合格的JSON字段,函數返回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甚至尚未在默認的errorError函數中出現,可是調用者能夠用它來生成帶有文件名和行號的錯誤信息。

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包, 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的語言特性和使用習慣鼓勵你在錯誤發生時作出明確的檢測(這和那些拋出異常的而後有時捕獲他們的語言有些區別)。在某些狀況,這種方式會形成Go代碼的冗餘,不過幸運的是咱們能使用一些技術來減小這種重複的捕獲操做。

考慮這樣一個App應用,這個應用有一個HTTP的處理函數,用來從數據庫接收數據而且將數據用模板格式化。

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

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengin.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormatValue("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.Excute方法返回的錯誤。這兩種狀況都返回帶Http狀態碼爲500的簡單的錯誤信息。上面的代碼看起來也很少,能夠接受,可是若是添加更多的 HTTP handlers狀況就不同了,你立刻會發現不少這樣的重複代碼來處理這些錯誤。

爲了減小這些重複的錯誤處理代碼,咱們能夠定義咱們本身的 HTTP AppHandler,讓它成一個帶着error返回值的類型:

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

而後咱們能夠更改viewRecord函數,讓它將錯誤返回:

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

這看起來比原始版本代碼的簡單了些, 可是 http 包並不能理解viewRecord函數返回的錯誤。這時咱們能夠經過實如今appHandler上的 http.Handler接口的方法 ServerHTTP來解決這個問題:

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方法而且將返回的錯誤展現給用戶。注意,ServeHTTP方法的接受者是一個函數。(go語言容許這樣作)這個方法經過表達式fn(w, r)來調用他的接受者,使ServeHTTP和appHandler關聯在一塊兒
如今,咱們在http包中註冊viewRecord時,使用了Hanlder函數(而不是HandlerFunc)。由於如今appHandler是一個http.Handler(而不是 http.HandlerFunc)。

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

經過構建一個特定的error做爲基礎構建,咱們可讓咱們的錯誤對用戶更友好。相對於僅僅將錯誤字符串展現給出來,返回帶有HTTP狀態碼的錯誤字符串是一個更好的展現方式,而且還能記錄下全部的錯誤信息以供App開發者調試用。

下面的代碼展現如何實現這種需求。咱們建立了一個包含error類型的和其餘類型的字段的appError結構體

type appError struct {
    Error   error
    Message string
    Code    int
}

下一步咱們修改appHandler類型,讓它返回 *appError值:

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

(一般,相對於返回一個error返回一個特定類型的錯誤是不對的,具體緣由能夠參考Go FQA , 可是在這裏是正確的,由於這個錯誤值只有ServeHTTP會用到它)

而後咱們讓appHandler的ServeHTTP方法將帶着HTTP狀態碼的appError錯誤信息展現給用戶,而且將全部錯誤信息展現給開發者終端。

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.ResponseWrite, 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跟原始版本有着相同的長度,可是如今這些放回信息都有特殊的信息,咱們提供了更爲友好的用戶體驗。

固然,這還不是最終的方案,咱們還能夠進一步提高咱們的application中的error處理方式。下面是改進的一些點:

  • 給錯誤handler提供一個漂亮的HTML模板
  • 若是用戶是超級用戶的話,添加堆疊追蹤到HTTP響應中,更方便調試
  • appError寫一個構造函數來存儲stack trace來讓開發者調試更方便
  • 恢復appHandler中的panic,用Critical級別的log將錯誤記錄到終端,同時告訴用戶"a serious error has occurred." 這是一個優雅的方式來避免將程序返回的難以理解的錯誤暴露給用戶。關於panic恢復,讀者能夠參考Defer, Panic, and Recover這篇文章來獲取更多的信息。

結論

適合的錯誤處理是一個好軟件最基本的要求。經過這篇文章中討論的技術,你應該能寫出更加可靠簡介的Go代碼。

參考資料:

Go by Example: Errors

相關文章
相關標籤/搜索