Go 部落格
錯誤處理與 Go
引言
如果您編寫過 Go 程式碼,很可能遇到過內建的 error
型別。Go 程式碼使用 error
值來指示異常狀態。例如,os.Open
函式在開啟檔案失敗時會返回一個非零值的 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
型別是一個介面型別。一個 error
變數代表任何可以描述自身為字串的值。這是介面的宣告:
type error interface {
Error() string
}
error
型別,與其他所有內建型別一樣,在 universe block 中是預宣告的。
最常用的 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
時,會收到一個非零值的 error
值(其具體表示是一個 errors.errorString
值)。呼叫者可以透過呼叫 error
的 Error
方法,或者直接列印它來訪問錯誤字串(“math: square root of…”)
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt 包透過呼叫其 Error() string
方法來格式化 error
值。
錯誤實現負責總結上下文。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 資料時遇到語法錯誤時會返回此型別。
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 程式碼變得冗長,但幸運的是,您可以使用一些技術來最大限度地減少重複的錯誤處理。
考慮一個 App Engine 應用程式,其中包含一個 HTTP 處理程式,該處理程式從資料儲存中檢索記錄並使用模板對其進行格式化。
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(“內部伺服器錯誤”)的簡單錯誤訊息。這看起來程式碼量 manageable,但新增更多 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 中恢復,將錯誤記錄到控制檯標記為“Critical”,同時告訴使用者“發生了嚴重錯誤”。這是一個很好的細節,可以避免向用戶暴露由程式設計錯誤引起的晦澀難懂的錯誤訊息。有關更多詳細資訊,請參閱 Defer、Panic 和 Recover 文章。
結論
適當的錯誤處理是優秀軟體的基本要求。透過採用本文介紹的技術,您應該能夠編寫更可靠、更簡潔的 Go 程式碼。
下一篇文章:App Engine 上的 Go 現已正式可用
上一篇文章:Go 中的頭等函式
部落格索引