Go 語言主要的設計準則是:簡潔、明白,簡潔是指語法和 C 類似,相當的簡單,明白是指任何語句都是很明顯的,不含有任何隱含的東西,在錯誤處理方案的設計中也貫徹了這一思想。我們知道在 C 語言裡面是透過回傳-1 或者 NULL 之類別的資訊來表示錯誤,但是對於使用者來說,不檢視相應的 API 說明文件,根本搞不清楚這個回傳值究竟代表什麼意思,比如 : 回傳 0 是成功,還是失敗,而 Go 定義了一個叫做 error 的型別,來明確的表達錯誤。在使用時,透過把回傳的 error 變數與 nil 的比較,來判定操作是否成功。例如os.Open
函式在開啟檔案失敗時將回傳一個不為 nil 的 error 變數
Copy func Open (name string ) (file * File , err error )
下面這個例子透過呼叫os.Open
開啟一個檔案,如果出現錯誤,那麼就會呼叫log.Fatal
來輸出錯誤資訊:
Copy f, err := os. Open ( "filename.ext" )
if err != nil {
log. Fatal (err)
}
類似於os.Open
函式,標準套件中所有可能出錯的 API 都會回傳一個 error 變數,以方便錯誤處理,這個小節將詳細地介紹 error 型別的設計,和討論開發 Web 應用中如何更好地處理 error。
Error 型別
error 型別是一個介面型別,這是它的定義:
Copy type error interface {
Error () string
}
error 是一個內建的介面型別,我們可以在/builtin/套件下面找到相應的定義。而我們在很多內部套件裡面用到的 error 是 errors 套件下面的實現的私有結構 errorString
Copy // errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e * errorString ) Error () string {
return e.s
}
你可以透過errors.New
把一個字串轉化為 errorString,以得到一個滿足介面 error 的物件,其內部實現如下:
Copy // New returns an error that formats as the given text.
func New (text string ) error {
return & errorString {text}
}
下面這個例子示範了如何使用errors.New
:
Copy 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 方法)被呼叫,以輸出錯誤,請看下面呼叫的範例程式碼:
Copy f, err := Sqrt ( - 1 )
if err != nil {
fmt. Println (err)
}
自訂 Error
透過上面的介紹我們知道 error 是一個 interface,所以在實現自己的套件的時候,透過定義實現此介面的結構,我們就可以實現自己的錯誤定義,請看來自 Json 套件的範例:
Copy type SyntaxError struct {
msg string // 錯誤描述
Offset int64 // 錯誤發生的位置
}
func (e * SyntaxError ) Error () string { return e.msg }
Offset 欄位在呼叫 Error 的時候不會被列印,但是我們可以透過型別斷言取得錯誤型別,然後可以列印相應的錯誤資訊,請看下面的例子:
Copy 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 型別,而非自訂錯誤型別,特別需要注意的是不應預宣告自訂錯誤型別的變數。例如:
Copy 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 套件採用的方法:
Copy package net
type Error interface {
error
Timeout () bool // Is the error a timeout?
Temporary () bool // Is the error temporary?
}
在呼叫的地方,透過型別斷言 err 是不是 net.Error,來細化錯誤的處理,例如下面的例子,如果一個網路發生臨時性錯誤,那麼將會 sleep 1 秒之後重試:
Copy if nerr, ok := err.( net . Error ); ok && nerr. Temporary () {
time. Sleep ( 1 e 9 )
continue
}
if err != nil {
log. Fatal (err)
}
錯誤處理
Go 在錯誤處理上採用了與 C 類似的檢查回傳值的方式,而不是其他多數主流語言採用的異常方式,這造成了程式碼編寫上的一個很大的缺點 : 錯誤處理程式碼的冗餘,對於這種情況是我們透過複用檢測函式來減少類似的程式碼。
請看下面這個例子程式碼:
Copy 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 詳解)。
Copy 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 )
}
}
上面我們定義了自訂的路由器,然後我們可以透過如下方式來註冊函式:
Copy func init () {
http. Handle ( "/view" , appHandler (viewRecord))
}
當請求/view 的時候我們的邏輯處理可以變成如下程式碼,和第一種實現方式相比較已經簡單了很多。
Copy 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 錯誤碼,然後顯示出來相應的錯誤程式碼,其實我們可以把這個錯誤資訊定義的更加友好,除錯的時候也方便定位問題,我們可以自訂回傳的錯誤型別:
Copy type appError struct {
Error error
Message string
Code int
}
這樣我們的自訂路由器可以改成如下方式:
Copy 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)
}
}
這樣修改完自訂錯誤之後,我們的邏輯處理可以改成如下方式:
Copy 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