go error處理

背景介紹

如果你有寫過(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)用errorError方法或者通過(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)的errorError函數(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代碼缀蹄。

參考資料:

Error handling and Go

Go by Example: Errors

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末峭跳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子袍患,更是在濱河造成了極大的恐慌坦康,老刑警劉巖竣付,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诡延,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡古胆,警方通過(guò)查閱死者的電腦和手機(jī)肆良,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逸绎,“玉大人惹恃,你說(shuō)我怎么就攤上這事」啄粒” “怎么了巫糙?”我有些...
    開(kāi)封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)颊乘。 經(jīng)常有香客問(wèn)我参淹,道長(zhǎng),這世上最難降的妖魔是什么乏悄? 我笑而不...
    開(kāi)封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任浙值,我火速辦了婚禮,結(jié)果婚禮上檩小,老公的妹妹穿的比我還像新娘开呐。我一直安慰自己,他們只是感情好规求,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布筐付。 她就那樣靜靜地躺著,像睡著了一般阻肿。 火紅的嫁衣襯著肌膚如雪瓦戚。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天冕茅,我揣著相機(jī)與錄音伤极,去河邊找鬼蛹找。 笑死,一個(gè)胖子當(dāng)著我的面吹牛哨坪,可吹牛的內(nèi)容都是我干的庸疾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼当编,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼届慈!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起忿偷,我...
    開(kāi)封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤金顿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鲤桥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體揍拆,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年茶凳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫂拴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贮喧,死狀恐怖筒狠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情箱沦,我是刑警寧澤辩恼,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站谓形,受9級(jí)特大地震影響灶伊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜套耕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一谁帕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冯袍,春花似錦匈挖、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至征冷,卻和暖如春择膝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背检激。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工肴捉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腹侣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓齿穗,卻偏偏與公主長(zhǎng)得像傲隶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窃页,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • 錯(cuò)誤處理 Go語(yǔ)言的錯(cuò)誤設(shè)計(jì)是通過(guò)返回值的方式來(lái)讓調(diào)用者對(duì)錯(cuò)誤進(jìn)行處理跺株,通常我們的處理是對(duì)error類型的返回值進(jìn)...
    Carrism閱讀 2,589評(píng)論 0 0
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,836評(píng)論 0 38
  • error code(錯(cuò)誤代碼)=0是操作成功完成。error code(錯(cuò)誤代碼)=1是功能錯(cuò)誤脖卖。error c...
    Heikki_閱讀 3,390評(píng)論 1 9
  • 斷了幾天沒(méi)記錄每日幸福事件乒省,沒(méi)記錄不代表沒(méi)有喲。 1畦木、昨夜貓兒點(diǎn)評(píng)作業(yè)袖扛,給了我一個(gè)精選,這等于可以獲得一枚優(yōu)秀勛章...
    少校了悟閱讀 250評(píng)論 3 2
  • 簡(jiǎn)書馋劈,粗淺的理解攻锰,簡(jiǎn)單的一封家書晾嘶,一封情書妓雾,一封屬于自己的人生感悟之書。 情不至所起垒迂,一往而深械姻,不知不覺(jué)間的一次點(diǎn)...
    天地飛皇閱讀 179評(píng)論 0 0