Go 錯(cuò)誤處理

2022-05-13 17:05 更新

Go語言主要的設(shè)計(jì)準(zhǔn)則是:簡潔、明白,簡潔是指語法和C類似,相當(dāng)?shù)暮唵危靼资侵溉魏握Z句都是很明顯的,不含有任何隱含的東西,在錯(cuò)誤處理方案的設(shè)計(jì)中也貫徹了這一思想。我們知道在C語言里面是通過返回-1或者NULL之類的信息來表示錯(cuò)誤,但是對于使用者來說,不查看相應(yīng)的API說明文檔,根本搞不清楚這個(gè)返回值究竟代表什么意思,比如:返回0是成功,還是失敗,而Go定義了一個(gè)叫做error的類型,來顯式表達(dá)錯(cuò)誤。在使用時(shí),通過把返回的error變量與nil的比較,來判定操作是否成功。例如os.Open函數(shù)在打開文件失敗時(shí)將返回一個(gè)不為nil的error變量

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

下面這個(gè)例子通過調(diào)用os.Open打開一個(gè)文件,如果出現(xiàn)錯(cuò)誤,那么就會(huì)調(diào)用log.Fatal來輸出錯(cuò)誤信息:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

類似于os.Open函數(shù),標(biāo)準(zhǔn)包中所有可能出錯(cuò)的API都會(huì)返回一個(gè)error變量,以方便錯(cuò)誤處理,這個(gè)小節(jié)將詳細(xì)地介紹error類型的設(shè)計(jì),和討論開發(fā)Web應(yīng)用中如何更好地處理error。

Error類型

error類型是一個(gè)接口類型,這是它的定義:

type error interface {
    Error() string
}

error是一個(gè)內(nèi)置的接口類型,我們可以在/builtin/包下面找到相應(yīng)的定義。而我們在很多內(nèi)部包里面用到的 error是errors包下面的實(shí)現(xiàn)的私有結(jié)構(gòu)errorString

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

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

你可以通過errors.New把一個(gè)字符串轉(zhuǎn)化為errorString,以得到一個(gè)滿足接口error的對象,其內(nèi)部實(shí)現(xiàn)如下:

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

下面這個(gè)例子演示了如何使用errors.New:

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

在下面的例子中,我們在調(diào)用Sqrt的時(shí)候傳遞的一個(gè)負(fù)數(shù),然后就得到了non-nil的error對象,將此對象與nil比較,結(jié)果為true,所以fmt.Println(fmt包在處理error時(shí)會(huì)調(diào)用Error方法)被調(diào)用,以輸出錯(cuò)誤,請看下面調(diào)用的示例代碼:

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

自定義Error

通過上面的介紹我們知道error是一個(gè)interface,所以在實(shí)現(xiàn)自己的包的時(shí)候,通過定義實(shí)現(xiàn)此接口的結(jié)構(gòu),我們就可以實(shí)現(xiàn)自己的錯(cuò)誤定義,請看來自Json包的示例:

type SyntaxError struct {
    msg    string // 錯(cuò)誤描述
    Offset int64  // 錯(cuò)誤發(fā)生的位置
}

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

Offset字段在調(diào)用Error的時(shí)候不會(huì)被打印,但是我們可以通過類型斷言獲取錯(cuò)誤類型,然后可以打印相應(yīng)的錯(cuò)誤信息,請看下面的例子:

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
}

需要注意的是,函數(shù)返回自定義錯(cuò)誤時(shí),返回值推薦設(shè)置為error類型,而非自定義錯(cuò)誤類型,特別需要注意的是不應(yīng)預(yù)聲明自定義錯(cuò)誤類型的變量。例如:

func Decode() *SyntaxError { // 錯(cuò)誤,將可能導(dǎo)致上層調(diào)用者err!=nil的判斷永遠(yuǎn)為true。
    var err *SyntaxError     // 預(yù)聲明錯(cuò)誤變量
    if 出錯(cuò)條件 {
        err = &SyntaxError{}
    }
    return err               // 錯(cuò)誤,err永遠(yuǎn)等于非nil,導(dǎo)致上層調(diào)用者err!=nil的判斷始終為true
}

原因見  http://golang.org/doc/faq#nil_error

上面例子簡單的演示了如何自定義Error類型。但是如果我們還需要更復(fù)雜的錯(cuò)誤處理呢?此時(shí),我們來參考一下net包采用的方法:

package net

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

在調(diào)用的地方,通過類型斷言err是不是net.Error,來細(xì)化錯(cuò)誤的處理,例如下面的例子,如果一個(gè)網(wǎng)絡(luò)發(fā)生臨時(shí)性錯(cuò)誤,那么將會(huì)sleep 1秒之后重試:

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

錯(cuò)誤處理

Go在錯(cuò)誤處理上采用了與C類似的檢查返回值的方式,而不是其他多數(shù)主流語言采用的異常方式,這造成了代碼編寫上的一個(gè)很大的缺點(diǎn):錯(cuò)誤處理代碼的冗余,對于這種情況是我們通過復(fù)用檢測函數(shù)來減少類似的代碼。

請看下面這個(gè)例子代碼:

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)
    }
}

上面的例子中獲取數(shù)據(jù)和模板展示調(diào)用時(shí)都有檢測錯(cuò)誤,當(dāng)有錯(cuò)誤發(fā)生時(shí),調(diào)用了統(tǒng)一的處理函數(shù)http.Error,返回給客戶端500錯(cuò)誤碼,并顯示相應(yīng)的錯(cuò)誤數(shù)據(jù)。但是當(dāng)越來越多的HandleFunc加入之后,這樣的錯(cuò)誤處理邏輯代碼就會(huì)越來越多,其實(shí)我們可以通過自定義路由器來縮減代碼(實(shí)現(xiàn)的思路可以參考第三章的HTTP詳解)。

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

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

上面我們定義了自定義的路由器,然后我們可以通過如下方式來注冊函數(shù):

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

當(dāng)請求/view的時(shí)候我們的邏輯處理可以變成如下代碼,和第一種實(shí)現(xiàn)方式相比較已經(jīng)簡單了很多。

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)
}

上面的例子錯(cuò)誤處理的時(shí)候所有的錯(cuò)誤返回給用戶的都是500錯(cuò)誤碼,然后打印出來相應(yīng)的錯(cuò)誤代碼,其實(shí)我們可以把這個(gè)錯(cuò)誤信息定義的更加友好,調(diào)試的時(shí)候也方便定位問題,我們可以自定義返回的錯(cuò)誤類型:

type appError struct {
    Error   error
    Message string
    Code    int
}

這樣我們的自定義路由器可以改成如下方式:

type appHandler func(http.ResponseWriter, *http.Request) *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)
    }
}

這樣修改完自定義錯(cuò)誤之后,我們的邏輯處理可以改成如下方式:

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
}

如上所示,在我們訪問view的時(shí)候可以根據(jù)不同的情況獲取不同的錯(cuò)誤碼和錯(cuò)誤信息,雖然這個(gè)和第一個(gè)版本的代碼量差不多,但是這個(gè)顯示的錯(cuò)誤更加明顯,提示的錯(cuò)誤信息更加友好,擴(kuò)展性也比第一個(gè)更好。

總結(jié)

在程序設(shè)計(jì)中,容錯(cuò)是相當(dāng)重要的一部分工作,在Go中它是通過錯(cuò)誤處理來實(shí)現(xiàn)的,error雖然只是一個(gè)接口,但是其變化卻可以有很多,我們可以根據(jù)自己的需求來實(shí)現(xiàn)不同的處理,最后介紹的錯(cuò)誤處理方案,希望能給大家在如何設(shè)計(jì)更好Web錯(cuò)誤處理方案上帶來一點(diǎn)思路。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)