譯|Errors are values

來源:cyningsun.github.io/08-19-2019/…html

Go程序員,尤爲是那些剛接觸語言的人,常見的討論點是如何處理錯誤。 談話常常變成對如下代碼段出現次數的失望git

if err != nil {
    return err
}
複製代碼

咱們最近掃描了咱們能夠找到的全部開源項目,發現這個代碼段每一頁或每兩頁只發生一次,比大家想象的更少。 儘管如此,若是必須老是寫程序員

if err != nuil
複製代碼

的感受持續存在, 必定是出了什麼問題,明顯的目標就是 Go 自己。github

這是使人遺憾和誤導性的,並且很容易糾正。事實可能正是Go 新程序員想問的:「如何處理錯誤?」,他們碰到這種模式,而後停在那裏。在其餘語言中,可使用 try-catch 塊或其餘此類機制來處理錯誤。所以,程序員認爲,當我使用舊語言的 try-catch 時,在 Go 中我只需輸入 if err != nil。隨着時間的推移,Go 代碼聚集了許多這樣的片斷,結果顯得很笨拙。golang

先無論這種解釋是否合適,很明顯這些 Go 程序員缺乏關於錯誤的一個根本點: Errors are values編程

值能夠編程,既然錯誤是值,所以錯誤也能夠編程。閉包

固然,涉及錯誤值的常見語句是檢測它是否爲nil,可是還有無數其餘能夠用錯誤值作的事情,而且應用其中的一些東西可使您的程序變得更好,從而消除大量若是機械的使用if語句檢查每一個錯誤會出現的樣板。less

如下是 bufioScanner 類型的一個簡單示例。它的 Scan 方法執行底層 I/O,這固然會致使錯誤。然而,該 Scan 方法根本不暴露錯誤。相反,它返回一個布爾值和一個單獨的方法,在掃描結束時運行,報告是否發生了錯誤。客戶端代碼以下所示:編程語言

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

固然,有出現錯誤的空值檢查,但它只出現並執行一次。 能夠將 Scan 方法定義爲函數

func (s *Scanner) Scan() (token []byte, error) 複製代碼

而後示例用戶代碼多是(取決於如何取回 token),

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

並無太大的不一樣,但有一個重要的區別。 在此代碼中,客戶端必須在每次迭代時檢查錯誤,但在真正的 Scanner API 中,錯誤處理從關鍵 API 元素抽象出來,而關鍵 API 元素正在迭代 token。 使用真正的 API,客戶端的代碼更天然:循環直到完成,最後進行錯誤處理。錯誤處理不會掩蓋控制流。

固然,幕後是,只要 Scan 遇到 I/O 錯誤,它就會記錄它並返回 false。 一個單獨的 Err 方法 在客戶端調用時報告錯誤值。 雖然很微不足道,但它與處處敲

if err != nil
複製代碼

或要求客戶端在每一個 token 以後檢查錯誤不一樣。它正在用錯誤值編程。簡潔的編程,對,仍仍是編程。

值得強調的是,不管設計如何,程序檢查錯誤都是相當重要的。這裏的討論不是關於如何避免檢查錯誤,而是關於使用語言,優雅的處理錯誤。

當我參加2014年秋季東京的 GoCon 時,出現了重複性錯誤檢查代碼的主題。一位熱心的Gopher,Twitter上稱呼爲 @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
複製代碼

代碼重複性很高。 在實際代碼中,會更長,還有更多內容,所以使用 helper 函數重構它並不容易,但在如此理想化的狀況下,封裝錯誤變量的函數字面值會有用:

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
}
複製代碼

該模式頗有效,但每一個執行寫操做的函數都須要一個閉包; 單獨的 helper 函數使用起來比較笨拙,由於 err 變量須要跨調用維護(試試看)。

經過借鑑上述 Scan 方法的想法,咱們可使代碼更清潔,更通用和可重複使用 。我在討論中提到過這種技術,但 @jxck 沒有明白如何應用它。通過長時間的交流,受到語言障礙的阻礙,我問我是否能夠借用他的筆記本電腦,經過寫一些代碼給他看。

我定義了一個名爲 errWriter 的對象,以下所示:

type errWriter struct {
    w   io.Writer
    err error
}
複製代碼

並給它一種方法,write。小寫部分是爲了突出區別, 它不須要有標準的 Write 簽名。該 write 方法調用底層 WriterWrite 方法 並記錄第一個錯誤以供未來引用:

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 包的 Writer 其實是 errWriter 想法的實現。 儘管 bufio.Writer.Write 返回錯誤,但主要是在於實現 io.Writer 接口。 bufio.WriterWrite 方法就像咱們上面的 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()
}
複製代碼

至少對於某些應用程序, 這種方法有一個明顯的缺點:在錯誤發生以前沒法知道完成了多少處理。 若是該信息很重要,則須要採用更細粒度的方法。 可是,一般,最後全有或全無檢查就足夠了。

咱們只研究了一種避免重複錯誤處理代碼的技術。 請記住,使用 errWriterbufio.Writer 並非簡化錯誤處理的惟一方法,而且這種方法並不適合全部狀況。 然而,關鍵的一課是 errors are values,而且Go編程語言的所有功能可用於處理它們。

使用語言簡化錯誤處理。

但請記住:不管你怎麼作,必定要檢查本身的錯誤!

最後,關於我與 @jxck 互動的完整故事,包括他錄製的一個小視頻,請訪問他的博客

相關文章
相關標籤/搜索