# 錯誤處理

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

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

下面這個例子透過呼叫`os.Open`開啟一個檔案，如果出現錯誤，那麼就會呼叫`log.Fatal`來輸出錯誤資訊：

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

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

## Error 型別

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

```go
type error interface {
    Error() string
}
```

error 是一個內建的介面型別，我們可以在/builtin/套件下面找到相應的定義。而我們在很多內部套件裡面用到的 error 是 errors 套件下面的實現的私有結構 errorString

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

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

你可以透過`errors.New`把一個字串轉化為 errorString，以得到一個滿足介面 error 的物件，其內部實現如下：

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

下面這個例子示範了如何使用`errors.New`:

```go
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 方法)被呼叫，以輸出錯誤，請看下面呼叫的範例程式碼：

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

## 自訂 Error

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

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

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

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

```go
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 型別，而非自訂錯誤型別，特別需要注意的是不應預宣告自訂錯誤型別的變數。例如：

```go
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 套件採用的方法：

```go
package net

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

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

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

## 錯誤處理

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

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

```go
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 詳解)。

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

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

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

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

```go
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 錯誤碼，然後顯示出來相應的錯誤程式碼，其實我們可以把這個錯誤資訊定義的更加友好，除錯的時候也方便定位問題，我們可以自訂回傳的錯誤型別：

```go
type appError struct {
    Error   error
    Message string
    Code    int
}
```

這樣我們的自訂路由器可以改成如下方式：

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

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

```go
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 錯誤處理方案上帶來一點思路。

## links

* [目錄](https://github.com/doggy8088/build-web-application-with-golang-zhtw/tree/80abfb9f0c5f9f703922f4114d78f2e3c26a4dfb/preface.md)
* 上一節: [錯誤處理，除錯和測試](https://willh.gitbook.io/build-web-application-with-golang-zhtw/11.0)
* 下一節: [使用 GDB 除錯](https://willh.gitbook.io/build-web-application-with-golang-zhtw/11.0/11.2)
