Go 部落格
錯誤處理與 Go
引言
如果你編寫過 Go 程式碼,你可能已經遇到過內建的 error
型別。Go 程式碼使用 error
值來表示異常狀態。例如,當 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)
}
// do something with the open *File f
僅僅瞭解 error
型別的這些知識,你就可以在 Go 中完成很多工作,但本文我們將仔細研究 error
,並討論 Go 中錯誤處理的一些良好實踐。
錯誤型別
error
型別是一個介面型別。一個 error
變量表示任何可以將其自身描述為字串的值。這是介面的宣告
type error interface {
Error() string
}
最常用的 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
函式構造其中一個值。它接受一個字串,將其轉換為 errors.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
傳遞負引數時,會收到一個非 nil 的 error
值(其具體表示是一個 errors.errorString
值)。呼叫者可以透過呼叫 error
的 Error
方法或直接列印它來訪問錯誤字串(“math: square root of…”)
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt 包透過呼叫 error
值的 Error() string
方法來格式化它。
錯誤實現有責任總結上下文。os.Open
返回的錯誤格式為“open /etc/passwd: permission denied”,而不僅僅是“permission denied”。我們的 Sqrt
返回的錯誤缺少有關無效引數的資訊。
為了新增這些資訊,一個有用的函式是 fmt
包的 Errorf
。它根據 Printf
的規則格式化字串,並將其作為由 errors.New
建立的 error
返回。
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
在許多情況下,fmt.Errorf
已經足夠好,但由於 error
是一個介面,你可以使用任意資料結構作為錯誤值,以便呼叫者可以檢查錯誤的詳細資訊。
例如,我們假設的呼叫者可能想要恢復傳遞給 Sqrt
的無效引數。我們可以透過定義一個新的錯誤實現而不是使用 errors.errorString
來實現這一點
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
一個複雜的呼叫者可以使用型別斷言來檢查 NegativeSqrtError
並特別處理它,而那些僅僅將錯誤傳遞給 fmt.Println
或 log.Fatal
的呼叫者將看到行為沒有變化。
再舉一個例子,json 包指定了一個 SyntaxError
型別,當 json.Decode
函式在解析 JSON blob 時遇到語法錯誤時會返回該型別。
type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() 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 專案中實際程式碼的略微簡化版本。)
error
介面只要求一個 Error
方法;特定的錯誤實現可能還有其他方法。例如,net 包按照通常的約定返回 error
型別的錯誤,但有些錯誤實現具有 net.Error
介面定義的附加方法
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
客戶端程式碼可以使用型別斷言測試 net.Error
,然後區分瞬態網路錯誤和永久性錯誤。例如,網路爬蟲在遇到臨時錯誤時可能會休眠並重試,否則就放棄。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
簡化重複的錯誤處理
在 Go 中,錯誤處理很重要。該語言的設計和約定鼓勵你在錯誤發生的地方顯式檢查錯誤(這與在其他語言中丟擲異常並有時捕獲它們的約定不同)。在某些情況下,這會使 Go 程式碼變得冗長,但幸運的是,你可以使用一些技術來最大限度地減少重複的錯誤處理。
考慮一個帶有 HTTP 處理程式的 App Engine 應用程式,該處理程式從資料儲存中檢索記錄並使用模板對其進行格式化。
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)
}
}
此函式處理 datastore.Get
函式和 viewTemplate
的 Execute
方法返回的錯誤。在這兩種情況下,它都向使用者顯示一個簡單的錯誤訊息,HTTP 狀態碼為 500(“內部伺服器錯誤”)。這看起來是一個可管理的少量程式碼,但新增更多 HTTP 處理程式後,你很快就會發現許多相同的錯誤處理程式碼副本。
為了減少重複,我們可以定義自己的 HTTP appHandler
型別,其中包含一個 error
返回值
type appHandler func(http.ResponseWriter, *http.Request) error
然後我們可以將 viewRecord
函式更改為返回錯誤
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)
}
這比原始版本更簡單,但 http 包不理解返回 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.Error(), 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 開發人員控制檯以進行除錯。
為此,我們建立了一個包含 error
和其他一些欄位的 appError
結構體
type appError struct {
Error error
Message string
Code int
}
接下來,我們修改 appHandler 型別以返回 *appError
值
type appHandler func(http.ResponseWriter, *http.Request) *appError
(通常將錯誤的具體型別而不是 error
返回回來是一個錯誤,原因在Go FAQ中討論過,但在這裡這樣做是正確的,因為 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(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
}
這個版本的 viewRecord
與原始版本長度相同,但現在每行都有特定的含義,並且我們提供了更友好的使用者體驗。
不止於此;我們可以進一步改進應用程式中的錯誤處理。一些想法
-
為錯誤處理程式提供一個漂亮的 HTML 模板,
-
當用戶是管理員時,透過將堆疊跟蹤寫入 HTTP 響應來簡化除錯,
-
為
appError
編寫一個建構函式,用於儲存堆疊跟蹤以方便除錯, -
從
appHandler
內部的 panic 中恢復,將錯誤記錄到控制檯為“嚴重”,同時告訴使用者“發生了嚴重錯誤”。這是一個很好的做法,可以避免將使用者暴露於因程式設計錯誤導致無法理解的錯誤訊息。有關更多詳細資訊,請參閱Defer、Panic 和 Recover 一文。
結論
正確的錯誤處理是優秀軟體的基本要求。透過採用本文中描述的技術,你應該能夠編寫更可靠、更簡潔的 Go 程式碼。
下一篇文章:Go for App Engine 現已全面推出
上一篇文章:Go 中的頭等函式
部落格索引