golang如何優雅的處理錯誤


錯誤是值 Errors are values

原文地址

Rob Pike
12 January 2015

在程序員中,尤其是go新手,經常聽到的一個討論話題是:如何處理錯誤。當下面這段代碼出現次數過多時,這個話題大多數時候都會變成對go的悲嘆。

if err != nil {
    return err
}

我們最近掃描了所有我們能找到的開源項目代碼,但是確發現這段代碼平均一兩頁出現一次,遠沒有我們原本設想的出現次數那么多。如果有人任然堅信一定要輸入if err !=nil,那么肯定是那里出錯了,顯然,是go本身的問題。

這是不幸,有誤,並且容易糾正的一個問題。可能當go新手在詢問”如何處理錯誤“的時候,他們學會了這個模式(指if err !=nil),並且對錯誤的認知到此為止了。在其他語言,一個程序員使用try-catch代碼塊或者而其他類似的機制去處理錯誤。因此,這些程序員們認為,在我之前的語言中我用try-catch去處理錯誤,那么我在go中我也要用相應的if err != nil處理錯誤就行了。隨着時間過去,在go代碼中出現許多這樣的小片段,導致看起來十分愚蠢。

無論這個解釋再怎么好,很明顯的一點就是go程序員忽略了一個關於錯誤的概念:錯誤是值(Errors are value)。

我們可以對值進行編程,並且因為錯誤是值,所以我們也可以對錯誤進行編程。

原文:Values can be programmed, and since errors are values, errors can be programmed.

一個常見的涉及到錯誤的語句是測試錯誤值是不是空值,但是我們還可以對錯誤值進行無數種處理,使用其中一些處理,可以讓我們的程序變得更好,消滅那些每一錯誤都要用樣板if處理的語句。

這里有一個簡單例子,來自bufio包的Scanner類,它的Scan方法執行基礎的I/O,這其中明顯可能產生錯誤,然而,scan方法並沒有一個錯誤,而是返回一個布爾值,並且在scan方法執行結束后,運行一個單獨的方法,報告是否單獨發生錯誤。客戶端代碼如下

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}

當然,可以對每個error進行一次空值檢查,但是它出現並執行一次,Scan方法也可以被定義如下

func (s *Scanner) Scan() (token []byte, error)

用戶的代碼樣例如下:

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

這兩個沒有多大的不同,但是有一個很重要的區別是,在后者的客戶端代碼,用戶必須在每次循環中都檢查一次錯誤,但是在真正的ScannerAPI中,只會對token進行迭代,錯誤處理從關鍵API代碼中抽象出來了。所以在真實的API中,用戶端的代碼因此更加自然,一直循環直到結束,然后再考慮錯誤,錯誤處理並不會影響到控制流。

在這背后發生了什么呢?當scan遇到了I/O錯誤,它會記錄並且返回false,另一個單獨的方法Err,報告這個錯誤當客戶端調用它的時候,雖然這有些瑣碎,但是不同於到處輸入if err != nil,或者告訴客戶必須時刻檢查每個token是否有錯誤。這個方法對錯誤進行了編程。簡單的編程處理,仍然是編程。

值得強調的是,無論使用哪種設計,最至關重要的是,無論錯誤如何暴露出來,都要去檢查處理它。我們在這的討論,不是討論如何避免檢查錯誤,而是如何使用go更優雅地處理錯誤。

我在東京參加2014年秋季GoCon大會時,反復出現了錯誤檢查代碼的話題。一個熱情的 gopher,@jxck,反映了他對錯誤處理的抱怨,他有一些像這樣的代碼:

_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

這出現了過多重復的代碼。在真實的編碼過程中,它更長些,並且有更多情況要處理,因此用一個輔助函數去重構它並不容易,但是在理想環境中,定義一個函數來處理錯誤變量或許能幫助我們

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

這個方式看起來不錯,但是在執行寫操作的時候,每個函數都需要閉包操作,每次單獨的輔助函數調用的時候都需要維護一個共同變量。

我們可以做得更干脆點,更適用點,並且復用性更高,通過借鑒上文的Scan方法,我向@jxck提到了這個技巧,但是他不知道如何去使用,在一段時間的交流后,由於語言障礙,我借用他的電腦,寫下了這樣一段代碼,試着去提醒他。

我定義了一個對象叫做errWriter,如下:

type errWriter struct {
    w   io.Writer
    err error
}

並且給他賦予一個方法write,他並不需要標准的Write方法簽名,並且他的小寫突出了區別。寫方法調用了Write方法,並且記錄了第一個遇到的錯誤用於后續使用。

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

當錯誤發生時,write將會變成一個無操作函數,但是會保存錯誤值。

定義errWriter類型,並且他的write方法,上述代碼可以被重構為

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

相比比起閉包的方法,這種寫法更為清晰且透明。再也不雜亂了,使用錯誤值編程,可以使得代碼更漂亮。

在一些標准包中,其他一些代碼也可以基於這個思想去構建,甚至直接使用errWriter

而且,errWriter的存在,還可以做更多的事情來提供幫助,特別是在不那么人工的例子中,他可以積累字節數,可以將寫操作合並到一個緩沖區中,以原子的方式操作。

事實上,這個方式經常出現在標准包匯總。archive/zipnet/http 都有使用到這個方法。本文的討論更為突出的表現了 bufio package's Writer 就是基於errWriter想法實現的,雖然 bufio.Writer.Write 返回一個錯誤,這主要是基於io.Writer接口。Writer方法的實現就像我們上文寫的errWriter.write方法,Flush報告錯誤,使得我們的例子可以如下:

b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
    return b.Flush()
}

至少對於某些應用程序,此方法存在一個明顯的缺點:無法知道發生錯誤之前已完成多少處理。如果該信息很重要,則必須采用更細粒度的方法。但是,通常,最后進行全有或全無的檢查就足夠了。

我們只研究了一種避免重復錯誤處理代碼的技術。請記住,使用 errWriter 或 bufio.Writer 並不是簡化錯誤處理的唯一方法,並且這種方法並不適合所有情況。但是,關鍵的教訓是,錯誤是值,而 Go 編程語言的全部功能可用於處理它們。

使用該語言簡化錯誤處理。

但是請記住:無論做什么,都要檢查錯誤!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM