背景介紹
如果你有寫過(guò)Go代碼汞扎,那么你可以會(huì)遇到Go中內(nèi)建類型error塑悼。Go語(yǔ)言使用error值來(lái)顯示異常狀態(tài)锻狗。例如掌腰,os.Open*在打開(kāi)文件錯(cuò)誤時(shí)帽揪,會(huì)返回一個(gè)非nil error值。
func Open(name string) (file *File, err error)
下面的代碼使用os.Open來(lái)打開(kāi)一個(gè)文件辅斟。如果出現(xiàn)錯(cuò)誤转晰,會(huì)調(diào)用log.Fatal打印出錯(cuò)誤的信息并且終止代碼。
f, err := os.Open("filename.etx")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
在使用Go的工作中士飒,上面的例子已經(jīng)能滿足大多數(shù)情況查邢,但是這篇文章會(huì)更進(jìn)一步的探討關(guān)于捕獲異常的實(shí)踐。
error類型
error類型是一個(gè)interface類型酵幕。一個(gè)error變量可以通過(guò)任何可以描述自己的string類型的值來(lái)展示自己扰藕。下面是它的接口描述:
type error interface {
Error() String
}
error類型,就像其他內(nèi)建類型一樣芳撒,==是在全局中預(yù)先聲明的==邓深。這意味著我們不用導(dǎo)入就可以在任何地方使用它。
最常用的error實(shí)現(xiàn)是在 errors 包中定義的一個(gè)不可導(dǎo)出的類型:errorString笔刹。
// errorString is a trivial implementation os error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
通過(guò)errors.New函數(shù)可以創(chuàng)建一個(gè)errorString實(shí)例.該函數(shù)接收一個(gè)string參數(shù)芥备,并將string參數(shù)轉(zhuǎn)換為一個(gè)erros.errorString,然后返回一個(gè)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, error.New("math: squara root of negative number")
}
// implementation
}
在調(diào)用Sqrt時(shí),如果傳入的參數(shù)是負(fù)數(shù)舌菜,調(diào)用者會(huì)接收到Sqrt返回的一個(gè)非空error值(正確來(lái)說(shuō)應(yīng)該是一個(gè)errors.errorString值)萌壳。調(diào)用者可以通過(guò)調(diào)用error的Error方法或者通過(guò)打印來(lái)得到錯(cuò)誤信息字段("math: squara root of nagative number")。
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
fmt包通過(guò)調(diào)用Error()方法來(lái)格式化error值
一個(gè)error接口的責(zé)任是總結(jié)錯(cuò)誤的內(nèi)容日月。os.Open的錯(cuò)誤返回的格式是像"open /etc/passwd: permission denied"這樣的格式, 而不僅僅只是"permission denied"袱瓮。Sqrt返回的錯(cuò)誤缺少了關(guān)于非法參數(shù)的信息。
為了讓信息更加明確爱咬,比較好用的一個(gè)函數(shù)是fmt包里面的Errorf尺借。它根據(jù)Printf的規(guī)則來(lái)函格式化一個(gè)字符串并且返回,就像使用errors.New創(chuàng)建的error值精拟。
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
很多情況下燎斩,fmt.Errorf已經(jīng)能夠滿足我們了虱歪,但是有時(shí)候我們還需要更多的細(xì)節(jié)。我們知道error是一個(gè)接口瘫里,因此你可以定義任意的數(shù)據(jù)類型來(lái)作為error值实蔽,以供調(diào)用者獲取更多的錯(cuò)誤細(xì)節(jié)荡碾。
例如谨读,如果有一個(gè)比較復(fù)雜的調(diào)用者想要恢復(fù)傳給Sqrt的非法參數(shù)。我們通過(guò)定義一個(gè)新的錯(cuò)誤實(shí)現(xiàn)而不是使用errors.errorString來(lái)實(shí)現(xiàn)這個(gè)需求:
type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %s", float64(f))
}
一個(gè)復(fù)雜的調(diào)用者就可以使用類型斷言(type assertion)來(lái)檢測(cè)NegativeSqrtError并且捕獲它坛吁,與此同時(shí)劳殖,對(duì)于使用fmt.Println或者log.Fatal來(lái)輸出錯(cuò)誤的方式來(lái)說(shuō)卻沒(méi)有改變他們的行為。
另一個(gè)例子來(lái)自json包拨脉,當(dāng)我們?cè)谑褂?strong>json.Decode函數(shù)時(shí)哆姻,如果我們傳入了一個(gè)不合格的JSON字段,函數(shù)返回SyntaxError類型錯(cuò)誤玫膀。
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甚至還沒(méi)有在默認(rèn)的error的Error函數(shù)中出現(xiàn),但是調(diào)用者可以用它來(lái)生成帶有文件名和行號(hào)的錯(cuò)誤信息帖旨。
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
}
(這是項(xiàng)目Camlistore中的代碼的一個(gè)簡(jiǎn)化版實(shí)現(xiàn))
內(nèi)置的error接口只需要實(shí)現(xiàn)Error方法箕昭;特定的error實(shí)現(xiàn)可能會(huì)添加其他的一些附加方法。例如net包, net包內(nèi)有很多種error類型解阅,通常跟常用的error一樣落竹,但是有些error實(shí)現(xiàn)添加一些附加方法,這些附加方法通過(guò)net.Error接口定義:
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
客戶端代碼可以通過(guò)類型斷言來(lái)檢測(cè)一個(gè)net.Error錯(cuò)誤以區(qū)分這是一個(gè)暫時(shí)性錯(cuò)網(wǎng)絡(luò)誤還是一個(gè)永久性錯(cuò)誤货抄。例如當(dāng)一個(gè)網(wǎng)絡(luò)爬蟲(chóng)遇到一個(gè)錯(cuò)誤時(shí)述召,如果是暫時(shí)性錯(cuò)誤,它會(huì)睡眠一下然后在重試蟹地,否則停止嘗試积暖。
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
簡(jiǎn)化捕獲重復(fù)的錯(cuò)誤
Go中,錯(cuò)誤捕獲是很重要的怪与。Go的語(yǔ)言特性和使用習(xí)慣鼓勵(lì)你在錯(cuò)誤發(fā)生時(shí)做出明確的檢測(cè)(這和那些拋出異常的然后有時(shí)捕獲他們的語(yǔ)言有些區(qū)別)呀酸。在某些情況,這種方式會(huì)造成Go代碼的冗余琼梆,不過(guò)幸運(yùn)的是我們能使用一些技術(shù)來(lái)減少這種重復(fù)的捕獲操作性誉。
考慮這樣一個(gè)App應(yīng)用,這個(gè)應(yīng)用有一個(gè)HTTP的處理函數(shù)茎杂,用來(lái)從數(shù)據(jù)庫(kù)接收數(shù)據(jù)并且將數(shù)據(jù)用模板格式化错览。
func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengin.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormatValue("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)
}
}
這個(gè)函數(shù)捕獲從datastore.Get函數(shù)和viewTemplate.Excute方法返回的錯(cuò)誤。這兩種情況都返回帶Http狀態(tài)碼為500的簡(jiǎn)單的錯(cuò)誤信息煌往。上面的代碼看起來(lái)也不多倾哺,可以接受轧邪,但是如果添加更多的 HTTP handlers情況就不一樣了,你馬上會(huì)發(fā)現(xiàn)很多這樣的重復(fù)代碼來(lái)處理這些錯(cuò)誤羞海。
為了減少這些重復(fù)的錯(cuò)誤處理代碼忌愚,我們可以定義我們自己的 HTTP AppHandler,讓它成一個(gè)帶著error返回值的類型:
type appHandler func(http.ResponseWriter, *http.Request) error
然后我們可以更改viewRecord函數(shù)却邓,讓它將錯(cuò)誤返回:
fun viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appending.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValie("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
這看起來(lái)比原始版本代碼的簡(jiǎn)單了些硕糊, 但是 http 包并不能理解viewRecord函數(shù)返回的錯(cuò)誤。這時(shí)我們可以通過(guò)實(shí)現(xiàn)在appHandler上的 http.Handler接口的方法 ServerHTTP來(lái)解決這個(gè)問(wèn)題:
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
ServeHTTP方法調(diào)用appHandler方法并且將返回的錯(cuò)誤展示給用戶腊徙。注意简十,ServeHTTP方法的接受者是一個(gè)函數(shù)。(go語(yǔ)言允許這樣做)這個(gè)方法通過(guò)表達(dá)式fn(w, r)來(lái)調(diào)用他的接受者撬腾,使ServeHTTP和appHandler關(guān)聯(lián)在一起
現(xiàn)在螟蝙,我們?cè)?strong>http包中注冊(cè)viewRecord時(shí),使用了Hanlder函數(shù)(而不是HandlerFunc)民傻。因?yàn)楝F(xiàn)在appHandler是一個(gè)http.Handler(而不是 http.HandlerFunc)胰默。
func init() {
http.Handle("/view", appHander(viewRecord))
}
通過(guò)構(gòu)建一個(gè)特定的error作為基礎(chǔ)構(gòu)建,我們可以讓我們的錯(cuò)誤對(duì)用戶更友好漓踢。相對(duì)于僅僅將錯(cuò)誤字符串展示給出來(lái)牵署,返回帶有HTTP狀態(tài)碼的錯(cuò)誤字符串是一個(gè)更好的展示方式,并且還能記錄下所有的錯(cuò)誤信息以供App開(kāi)發(fā)者調(diào)試用彭雾。
下面的代碼展示如何實(shí)現(xiàn)這種需求碟刺。我們創(chuàng)建了一個(gè)包含error類型的和其他類型的字段的appError結(jié)構(gòu)體
type appError struct {
Error error
Message string
Code int
}
下一步我們修改appHandler類型,讓它返回 ** appError*值:
type appHandler func(http.ResponseWriter, *http.Request) * appError
(通常薯酝,相對(duì)于返回一個(gè)error返回一個(gè)特定類型的錯(cuò)誤是不對(duì)的半沽,具體原因可以參考Go FQA , 但是在這里是正確的,因?yàn)檫@個(gè)錯(cuò)誤值只有ServeHTTP會(huì)用到它)
然后我們讓appHandler的ServeHTTP方法將帶著HTTP狀態(tài)碼的appError錯(cuò)誤信息展示給用戶吴菠,并且將所有錯(cuò)誤信息展示給開(kāi)發(fā)者終端者填。
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的代碼做葵,讓它遇到錯(cuò)誤時(shí)返回更多的內(nèi)容:
func viewRecord(w http.ResponseWrite, 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
}
這個(gè)版本的viewRecord跟原始版本有著相同的長(zhǎng)度占哟,但是現(xiàn)在這些放回信息都有特殊的信息,我們提供了更為友好的用戶體驗(yàn)酿矢。
當(dāng)然榨乎,這還不是最終的方案,我們還可以進(jìn)一步提升我們的application中的error處理方式瘫筐。下面是改進(jìn)的一些點(diǎn):
- 給錯(cuò)誤handler提供一個(gè)漂亮的HTML模板
- 如果用戶是超級(jí)用戶的話蜜暑,添加堆疊追蹤到HTTP響應(yīng)中,更方便調(diào)試
- 為appError寫一個(gè)構(gòu)造函數(shù)來(lái)存儲(chǔ)stack trace來(lái)讓開(kāi)發(fā)者調(diào)試更方便
- 恢復(fù)appHandler中的panic策肝,用Critical級(jí)別的log將錯(cuò)誤記錄到終端肛捍,同時(shí)告訴用戶"a serious error has occurred." 這是一個(gè)優(yōu)雅的方式來(lái)避免將程序返回的難以理解的錯(cuò)誤暴露給用戶隐绵。關(guān)于panic恢復(fù),讀者可以參考Defer, Panic, and Recover這篇文章來(lái)獲取更多的信息拙毫。
結(jié)論
適合的錯(cuò)誤處理是一個(gè)好軟件最基本的要求依许。通過(guò)這篇文章中討論的技術(shù),你應(yīng)該能寫出更加可靠簡(jiǎn)介的Go代碼缀蹄。