Files
2019-06-22 23:41:28 +08:00

242 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 11.1 錯誤處理
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
* [目錄](<preface.md>)
* 上一節:[錯誤處理,除錯和測試](<11.0.md>)
* 下一節:[使用 GDB 除錯](<11.2.md>)