Files
build-web-application-with-…/zh-tw/11.1.md
2019-02-26 01:40:54 +08:00

8.7 KiB
Raw Blame History

11.1 錯誤處理

Go語言主要的設計準則是簡潔、明白簡潔是指語法和C類似相當的簡單明白是指任何語句都是很明顯的不含有任何隱含的東西在錯誤處理方案的設計中也貫徹了這一思想。我們知道在C語言裡面是透過返回-1或者NULL之類別的資訊來表示錯誤但是對於使用者來說不檢視相應的API說明文件根本搞不清楚這個返回值究竟代表什麼意思比如:返回0是成功還是失敗,而Go定義了一個叫做error的型別來顯式表達錯誤。在使用時透過把返回的error變數與nil的比較來判定操作是否成功。例如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)
}

類似於os.Open函式標準套件中所有可能出錯的API都會返回一個error變數以方便錯誤處理這個小節將詳細地介紹error型別的設計和討論開發Web應用中如何更好地處理error。

Error型別

error型別是一個介面型別這是它的定義


type error interface {
	Error() string
}

error是一個內建的介面型別我們可以在/builtin/套件下面找到相應的定義。而我們在很多內部套件裡面用到的 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把一個字串轉化為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的時候傳遞的一個負數然後就得到了non-nil的error物件將此物件與nil比較結果為true所以fmt.Println(fmt套件在處理error時會呼叫Error方法)被呼叫,以輸出錯誤,請看下面呼叫的示例程式碼:


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

自訂Error

透過上面的介紹我們知道error是一個interface所以在實現自己的套件的時候透過定義實現此介面的結構我們就可以實現自己的錯誤定義請看來自Json套件的示例


type SyntaxError struct {
	msg    string // 錯誤描述
	Offset int64  // 錯誤發生的位置
}

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

Offset欄位在呼叫Error的時候不會被列印但是我們可以透過型別斷言取得錯誤型別然後可以列印相應的錯誤資訊請看下面的例子:


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
}

需要注意的是函式返回自訂錯誤時返回值推薦設定為error型別而非自訂錯誤型別特別需要注意的是不應預宣告自訂錯誤型別的變數。例如


func Decode() *SyntaxError { // 錯誤將可能導致上層呼叫者err!=nil的判斷永遠為true。
        var err *SyntaxError     // 預宣告錯誤變數
        if 出錯條件 {
            err = &SyntaxError{}
        }
        return err               // 錯誤err永遠等於非nil導致上層呼叫者err!=nil的判斷始終為true
    }

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

上面例子簡單的示範瞭如何自訂Error型別。但是如果我們還需要更復雜的錯誤處理呢此時我們來參考一下net套件採用的方法


package net

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

在呼叫的地方透過型別斷言err是不是net.Error,來細化錯誤的處理例如下面的例子如果一個網路發生臨時性錯誤那麼將會sleep 1秒之後重試


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

錯誤處理

Go在錯誤處理上採用了與C類似的檢查返回值的方式而不是其他多數主流語言採用的異常方式這造成了程式碼編寫上的一個很大的缺點:錯誤處理程式碼的冗餘,對於這種情況是我們透過複用檢測函式來減少類似的程式碼。

請看下面這個例子程式碼:


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

上面的例子中取得資料和範本展示呼叫時都有檢測錯誤,當有錯誤發生時,呼叫了統一的處理函式http.Error返回給客戶端500錯誤碼並顯示相應的錯誤資料。但是當越來越多的HandleFunc加入之後這樣的錯誤處理邏輯程式碼就會越來越多其實我們可以透過自訂路由器來縮減程式碼(實現的思路可以參考第三章的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)
	}
}

上面我們定義了自訂的路由器,然後我們可以透過如下方式來註冊函式:


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

當請求/view的時候我們的邏輯處理可以變成如下程式碼和第一種實現方式相比較已經簡單了很多。


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

上面的例子錯誤處理的時候所有的錯誤返回給使用者的都是500錯誤碼然後打印出來相應的錯誤程式碼其實我們可以把這個錯誤資訊定義的更加友好除錯的時候也方便定位問題我們可以自訂返回的錯誤型別


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

這樣修改完自訂錯誤之後,我們的邏輯處理可以改成如下方式:


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的時候可以根據不同的情況取得不同的錯誤碼和錯誤資訊雖然這個和第一個版本的程式碼量差不多但是這個顯示的錯誤更加明顯提示的錯誤資訊更加友好擴充套件性也比第一個更好。

總結

在程式設計中容錯是相當重要的一部分工作在Go中它是透過錯誤處理來實現的error雖然只是一個介面但是其變化卻可以有很多我們可以根據自己的需求來實現不同的處理最後介紹的錯誤處理方案希望能給大家在如何設計更好Web錯誤處理方案上帶來一點思路。