這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Golang 錯誤處理的 Panic – Recover 模型確實不太一樣,Go 團隊的部落格上寫了一篇相對完整的介紹這個模型使用的文章“Error handling and Go”。我覺得挺好,故翻譯於此。本應早就完成這個翻譯了,不過由於公司重組等等原因,一直留了首尾沒能處理完整。所以拖到了今天,真是不應該啊!
————————翻譯分割線————————–
錯誤處理和Go
如果你已經編寫過 Go 代碼,可能已經遇到過 os.Error 類型了。Go 代碼使用 os.Error 值來標示異常狀態。例如,當 os.Open 函數開啟檔案失敗時,返回一個非 nil 的 os.Error 值。
func Open(name string) (file *File, err Error)
下面的函數使用 os.Open 開啟一個檔案。如果產生了錯誤,它會調用 log.Fatal 列印錯誤資訊並且中斷運行。
func main() { f, err := os.Open("filename.ext") if err != nil { log.Fatal(err) } // 對開啟的 *File f 做些事情}
在 Go 中只要知道了 os.Error 就可以做很多事情了,不過在這篇文章中,我們會更進一步瞭解 os.Error 並探討一些 Go 中錯誤處理比較好的方法。
錯誤類型
os.Error 類型是一個介面類型。os.Error變數可以是任何可以將其描繪成字串的值。這裡是介面的定義:
package ostype Error interface { String() string}
對於 os.Error 沒什麼特別的。只是一個廣泛使用的約定而已。
最一般的 os.Error 實現是 os 包的未匯出的 errorString 類型。
type errorString stringfunc (s errorString) String() string { return string(s) }
可以通過 os.NewError 函數構建一個這樣的值。它接受一個字串,然後轉換成 os.errorString 並且返回一個 os.Error 值。
func NewError(s string) Error { return errorString(s) }
這裡示範了使用 os.NewError 的一種可能:
func Sqrt(f float64) (float64, os.Error) { if f < 0 { return 0, os.NewError("math: square root of negative number") } // 實現}
調用方向 Sqrt 傳遞了錯誤的參數,會得到一個非 nil 的 os.Error 值(實際上是重新表達的一個 os.errorString 值)。調用者可以通過調用 os.Error 的 String 方法得到錯誤字串,或者僅僅是列印出來:
f, err := Sqrt(-1)if err != nil { fmt.Println(err)}
fmt 包可以列印任何帶有 String() string 方法的內容,這包括了 os.Error 值。
這是錯誤實現用於概述上下文環境的一種職責。os.Open 返回一個格式化的錯誤,如“open /etc/passwd: permission denied,”而不僅僅是“permission denied.”Sqrt 返回的錯誤中缺失了關於非法參數的資訊。
為了添加這個資訊,在 fmt 包中有一個很有用的函數 Errorf。它將一個字串依照 Printf 的規則進行格式化,然後將其返回成為 os.NewError 建立的 os.Error 類型。
if f < 0 { return 0, fmt.Errorf("math: square root of negative number %g", f)}
在大多數情況下 fmt.Errorf 已經足夠好了,但是由於 os.Error 是一個介面,也可以使用更加詳盡的資料結構作為錯誤值,以便讓調用者檢查錯誤的細節。
例如,假設一個使用者希望找到傳遞到 Sqrt 的非法參數。可以通過定義一個新的錯誤實現代替 os.errorString 來做到這點:
type NegativeSqrtError float64func (f NegativeSqrtError) String() string { return fmt.Sprintf(“math: square root of negative number %g”, float64(f))}
一個有經驗的調用者可以使用類型斷言來檢查 NegativeSqrtError 並且特別處理它,僅僅將錯誤傳遞給 fmt.Println 或者 log.Fatal 是不會有任何行為上的改變。
另一個例子,json 包指定 json.Decode 函數返回 SyntaxError 類型,當解析一個 JSON blob 發生語法錯誤的時候。
type SyntaxError struct { msg string // 描述錯誤 Offset int64 // 錯誤在讀取了 Offset 位元組後發生}func (e *SyntaxError) String() string { return e.msg }
Offset 欄位沒有顯示在錯誤預設的格式中,但是調用者可以使用它來添加檔案和行資訊到其錯誤訊息中:
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項目的實際的代碼。)
os.Error 介面僅僅需要一個 String 方法;特別的錯誤實現可能有一些附加的方法。例如,net 包按照慣例返回 os.Error
類型,但是一些錯誤實現包含由 net.Error 定義的附加方法:
package nettype Error interface { os.Error Timeout() bool // 是逾時錯誤嗎? Temporary() bool // 是臨時性錯誤嗎?}
用戶端代碼可以用類型斷言來測試 net.Error,這樣就可以從持久性錯誤中找到臨時性的錯誤。例如,一個 Web 爬蟲可能會在遇到臨時性錯誤時休眠然後重試,持久錯誤的話就徹底放棄。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() { time.Sleep(1e9) continue}if err != nil { log.Fatal(err)}
簡化重複的錯誤處理
在 Go 中,錯誤處理是重要的。這個語言的設計和規範鼓勵對產生錯誤的地方進行明確的檢查(這與其他語言拋出異常,然後在某個時候才處理它們是有區別的)。在某些情況下,這使得 Go 的代碼很羅嗦,不過幸運的是有一些讓錯誤處理儘可能少重複的技術可以使用。
考慮 App Engine 應用,在 HTTP 處理時從資料存放區擷取記錄,然後通過模板進行格式化。
func init() { http.HandleFunc("/view", viewRecord)}func viewRecord(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) key := datastore.NewKey("Record", r.FormValue("id"), 0, nil) record := new(Record) if err := datastore.Get(c, key, record); err != nil { http.Error(w, err.String(), 500) return } if err := viewTemplate.Execute(w, record); err != nil { http.Error(w, err.String(), 500) }}
這個函數處理了由 datastore.Get 函數和 viewTemplate 的 Execute 方法返回的錯誤。在兩種情況下,它都是簡單的返回一個錯誤訊息給使用者,用 HTTP 狀態碼 500(“Internal Server Error”)。這代碼看起來是可以改進的,只需添加一些 HTTP 處理,然後就可以結束掉這種有許多相同的錯誤處理代碼的狀況。
可以自訂 HTTP 處理 appHandler 類型,包括返回一個 os.Error 值來減少重複:
type appHandler func(http.ResponseWriter, *http.Request) os.Error
然後修改 viewRecord 函數返回錯誤:
func viewRecord(w http.ResponseWriter, r *http.Request) os.Error { c := appengine.NewContext(r) key := datastore.NewKey("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)}
這比原來的版本簡單,但是 http 包不明白返回 os.Error 的函數。為了修複這個問題,可以在 appHandler 上實現一個 http.Handler 介面的 ServeHTTP 方法:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { http.Error(w, err.String(), 500) }}
ServeHTTP 方法調用 appHandler 函數,並且給使用者顯示返回的錯誤(如果有的話)。注意這個方法的接收者——fn,是一個函數。(Go 可以這樣做!)方法調用運算式 fn(w, r) 中定義的接收者。
現在當向 http 包註冊了 viewRecord,就可以使用 Handle 函數(代替 HandleFunc)appHandler 作為一個 http.Handler(而不是一個 http.HandlerFunc)。
func init() { http.Handle("/view", appHandler(viewRecord))}
通過這樣在基礎架構中的錯誤處理,可以使其對使用者更加友好。除了僅僅顯示一個錯誤字串,給使用者一些簡單的錯誤資訊以及適當的 HTTP 狀態代碼會更好,同時在 App Engine 開發人員控制台記錄完整的錯誤用於調試。
為了做到這點,建立一個 appError 結構包含 os.Error 和一些其他欄位:
type appError struct { Error os.Error Message string Code int}
接下來我們修改 appHandler 類型返回 *appError 值:
type appHandler func(http.ResponseWriter, *http.Request) *appError
(通常,錯誤資訊不使用 os.Error 而是使用實際類型進行傳遞的做法是錯誤的,在將發表的文章裡會討論這個,不過在這裡是正確的,因為ServeHTTP 是唯一看到這個值並且使用其內容的地方。)
並且編寫 appHandler 的 ServeHTTP 方法顯示 appError 的 Message 和對應的 HTTP 狀態 Code 給使用者,同時記錄完整的 Error 到開發人員控制台:
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.ResponseWriter, r *http.Request) *appError { c := appengine.NewContext(r) key := datastore.NewKey("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 與之前的長度類似,但是現在每行都有特別的含義,並且提供了對使用者更加友好的體驗。
這還沒有結束;還可以進一步在應用中改進錯誤處理。有一些思路:
- 為錯誤處理提供一個漂亮的 HTML 範本,
- 當使用者是管理員時,將棧跟蹤輸出到 HTTP 的響應中,以方便調試,
- 編寫一個 appError 的建構函式,儲存棧跟蹤使得調試更容易,
- 在 appHandler 裡從 panic 中 recover,將錯誤作為“嚴重異常”記錄進開發人員控制台,而只簡單的告訴使用者“發生了一個嚴重的錯誤”。 這是避免向使用者暴露由於編碼錯誤引起的不可預料的錯誤的資訊的一個不錯的想法。參看 Defer, Panic, and Recover 文章瞭解更多細節。
總結
適當的錯誤處理是好軟體的基本需要。根據本文所討論的技術,就可以編寫出更加可靠和簡介的 Go 代碼。