if err != nil 太煩?Go 創始人教你如何對錯誤進行程式設計!

語言: CN / TW / HK

大家好,我是煎魚。

前段時間我分享了一篇文章《10+ 條 Go 官方諺語,你知道幾條?》,引發了許多小夥伴的討論。其中有一條 “Errors are values”,大家在是 “錯誤是值” 還是 “錯誤就是價值” 中反覆橫跳,糾結不易。

其實說這句話的 Rob Pike,他用一篇文章《Errors are values》詮釋了這句諺語的意思,到底是什麼?

今天煎魚和大家一起學習,以下的 “我” 均代表 Rob Pike。

背景

Go 程式設計師,尤其是那些剛接觸該語言的程式設計師,經常討論的一個問題是如何處理錯誤。對於以下程式碼片段出現的次數,談話經常變成哀嘆(各大平臺吐槽、批判非常多,認為設計的不好)。

如下程式碼:

go if err != nil { return err }

掃描程式碼片段

我們最近掃描了我們能找到的所有 Go 開源專案,發現這個程式碼片段只在每一兩頁出現一次,比一些人認為的要少。

儘管如此,如果人們仍然認為必須經常輸入如下程式碼:

go if err != nil

那麼一定有什麼地方出了問題,而明顯的目標就是 Go 語言本身(說設計的不好?)。

錯誤的理解

顯然這是不幸的,誤導的,而且很容易糾正。也許現在的情況是,剛接觸 Go 的程式設計師會問:"如何處理錯誤?",學習這種模式,然後就此打住。

在其他語言中,人們可能會使用 try-catch 塊或其他類似機制來處理錯誤。因此,程式設計師認為,當我在以前的語言中會使用 try-catch 時,我在 Go 中只需輸入 if err != nil。

隨著時間的推移,Go 程式碼中收集了許多這樣的片段,結果感覺很笨拙。

錯誤是值

不管這種解釋是否合適,很明顯,這些 Go 程式設計師錯過了關於錯誤的一個基本點:錯誤是值(Errors are values)。

值可以被程式設計,既然錯誤是值,那麼錯誤也可以被程式設計。

當然,涉及錯誤值的常見語句是測試它是否為 nil,但是還有無數其他事情可以用錯誤值做,並且應用其中一些其他事情可以使您的程式更好,消除很多樣板。

如果使用死記硬背的 if 語句檢查每個錯誤,就會出現這種情況(也就是 if err != nil 到處都是的情況)。

bufio 例子

下面是一個來自 bufio 包的 Scanner 型別的簡單例子。它的 Scan 方法執行了底層的 I/O,這當然會導致一個錯誤。然而,Scan 方法根本沒有暴露出錯誤。

相反,它返回一個布林值,並在掃描結束時執行一個單獨的方法,報告是否發生錯誤。

客戶端程式碼看起來像這樣:

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

當然,有一個 nil 檢查錯誤,但它只出現並執行一次。Scan 方法可以改為定義為:

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

然後,使用者程式碼的例子可能是(取決於如何檢索令牌):

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

這並沒有太大的不同,但有一個重要的區別。在這段程式碼中,客戶端必須在每次迭代時檢查錯誤,但在真正的 Scanner API 中,錯誤處理是從關鍵 API 元素中抽象出來的,它正在迭代令牌。

使用真正的 API,客戶端的程式碼因此感覺更自然:迴圈直到完成,然後擔心錯誤。

錯誤處理不會掩蓋控制流程。

當然,在幕後發生的事情是,一旦 Scan 遇到 I/O 錯誤,它就會記錄它並返回 false。當客戶端詢問時,一個單獨的方法 Err 會報告錯誤值。

雖然這很微不足道,但它與在每個 if err != nil 後到處放或要求客戶端檢查錯誤是不一樣的。這是用錯誤值程式設計。簡單的程式設計,是的,但仍然是程式設計。

值得強調的是,無論設計如何,程式檢查錯誤是至關重要的,無論它們暴露在哪裡。 這裡的討論不是關於如何避免檢查錯誤,而是關於使用語言優雅地處理錯誤。

實戰探討

當我參加在東京舉行的 2014 年秋季 GoCon 時,出現了重複錯誤檢查程式碼的話題。一位熱心的 Gopher,在 Twitter 上的名字是 @jxck_,迴應了我們熟悉的關於錯誤檢查的哀嘆。

他有一些程式碼,從結構上看是這樣的:

go _, 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

它是非常重複的。在真正的程式碼中,這段程式碼比較長,有更多的事情要做,所以不容易只是用一個輔助函式來重構這段程式碼,但在這種理想化的形式中,一個函式字面的關閉對錯誤變數會有幫助:

go 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 }

這種模式效果很好,但需要在每個執行寫入的函式中關閉; 單獨的輔助函式使用起來比較笨拙,因為需要在呼叫之間維護 err 變數(嘗試一下)。

我們可以通過借用上面的掃描方法的思路,使之更簡潔、更通用、更可重複使用。我在我們的討論中提到了這個技術,但是 @jxck_ 沒有看到如何應用它。經過長時間的交流,在語言不通的情況下,我問能不能借他的筆記本,打一些程式碼給他看。

我定義了一個名為 errWriter 的物件,如下所示:

go type errWriter struct { w io.Writer err error }

並給了它一種方法,Write。 它不需要具有標準的 Write 簽名,並且部分小寫以突出區別。 write 方法呼叫底層 Writer 的 Write 方法,並記錄第一個錯誤以備參考:

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

一旦發生錯誤,Write 方法就會變成無用功,但錯誤值會被儲存。

鑑於 errWriter 型別和它的 Write 方法,上面的程式碼可以被重構為如下程式碼:

go 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/zip 和 net/http 包使用它。 在這個討論中更突出的是,bufio 包的 Writer 實際上是 errWriter 思想的一個實現。 儘管 bufio.Writer.Write 返回錯誤,但這主要是為了尊重 io.Writer 介面。

bufio.Writer 的 Write 方法的行為就像我們上面的 errWriter.write 方法一樣,Flush 會報錯,所以我們的例子可以這樣寫:

go 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 程式語言的全部功能可用於處理它們。

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

但請記住:無論您做什麼,都要檢查您的錯誤!

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

Go 圖書系列

更多閱讀