1. 程式人生 > 程式設計 >譯|Errors are values

譯|Errors are values

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

Go程式設計師,尤其是那些剛接觸語言的人,常見的討論點是如何處理錯誤。 談話經常變成對以下程式碼段出現次數的失望

if err != nil {
    return err
}
複製程式碼

我們最近掃描了我們可以找到的所有開源專案,發現這個程式碼段每一頁或每兩頁只發生一次,比你們想象的更少。 儘管如此,如果必須總是寫

if err != nuil
複製程式碼

的感覺持續存在,一定是出了什麼問題,明顯的目標就是 Go 本身。

這是令人遺憾和誤導性的,而且很容易糾正。事實可能正是Go 新程式設計師想問的:“如何處理錯誤?”,他們碰到這種模式,然後停在那裡。在其他語言中,可以使用 try-catch

塊或其他此類機制來處理錯誤。因此,程式設計師認為,當我使用舊語言的 try-catch 時,在 Go 中我只需輸入 if err != nil。隨著時間的推移,Go 程式碼彙集了許多這樣的片段,結果顯得很笨拙。

先不管這種解釋是否合適,很明顯這些 Go 程式設計師缺少關於錯誤的一個根本點: Errors are values

值可以程式設計,既然錯誤是值,因此錯誤也可以程式設計。

當然,涉及錯誤值的常見語句是檢測它是否為nil,但是還有無數其他可以用錯誤值做的事情,並且應用其中的一些東西可以使您的程式變得更好,從而消除大量如果機械的使用if語句檢查每個錯誤會出現的樣板。

以下是 bufio

Scanner 型別的一個簡單示例。它的 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 互動的完整故事,包括他錄製的一個小視訊,請訪問他的部落格