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