Errors are values
原文地址?https://blog.golang.org/errors-are-values
Go程序員之間(特別是這些剛接觸Go語言的新人)一個常見的討論點是如何處理錯誤。談話經常變成為對如下代碼序列出現次數的感嘆。
If err!=nil{
??return err
}
我們最近掃描了我們可以找到的所有開源項目,并發現這個代碼段每頁或每兩頁只出現一次,比你相信的要少。然而,如果這種看法仍然存在:所有時候都必須輸入
If err!=nil
一定有些地方是錯誤的,明顯的目標就是Go本身。
?
這是不幸的,誤導的,很容易糾正的。也許正在發生的事情是新的程序員問道“如何處理錯誤?”,學習這種模式,并停止在那里。在其他語言中,可能會使用try-catch塊或其他這樣的機制來處理錯誤。因此,程序員認為,當我用舊的語言使用try-catch時,我將在Go中輸入err!=nil。隨著時間的推移,Go代碼收集了很多這樣的片段,結果感覺很笨拙。
無論這個解釋是否合適,很明顯,這些Go程序員錯過了關于errors的基本觀點:錯誤是值(Errors are values)
值可編程,并且由于錯誤是值,所以可以對錯誤進行編程。
當然,一個涉及錯誤值的常見語句是測試它是否為nil,但是還有無數的其他東西可以使用錯誤值,并且應用某些其他的東西可以使你的程序更好,消除了大量樣板(如果每個error通過一個死記硬背的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
}
?
當然,這里有個對err的nil檢查,但它似乎只執行一次。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
}
?
這不是很大的不同,但有一個重要的區別。在此代碼中,客戶端必須在每次迭代時檢查錯誤,但在實際的Scan API中,將錯誤處理從關鍵的API元素中抽象出來,在token上迭代。使用真實的API,客戶端代碼感覺更自然:循環直到完成,然后判斷錯誤。錯誤處理不會干擾控制流。
?看內部發生了什么,當然是,一旦Scan遇到I/O錯誤,它會記錄它并返回false。一個單獨的方法Err會在客戶端詢問時報告錯誤值。簡而言之,這與把 if err != nil放到任何地方或要求客戶端在每個token后檢查錯誤的做法是不一樣的。它是用錯誤值進行編程。簡單的編程,是的,但仍然編程。
值得強調的是,無論設計如何,程序檢查errors是非常重要的,然而它們是暴露出來的。這里的討論不是關于如何避免檢查錯誤,它是關于使用語言來優雅地處理錯誤。
?
當我在東京參加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_沒有看懂如何應用它。經過長時間的交流,收到語言障礙的阻礙,我問道我是否可以借用他的筆記本電腦,并通過鍵入一些代碼來給他看。
我定義了一個名為errWrite的對象,如下所示:
type errWriter struct {
????w ??io.Writer
????err error
}
?
并給它一個方法,write。它不需要具有標準的Write簽名,小寫部分來突出顯示區別。write方法調用底層Writer的Write方法,并記錄第一個錯誤以供將來參考:
?
func (ew *errWriter) write(buf []byte) {
????if ew.err != nil {
????????return
????}
????_, ew.err = ew.w.Write(buf)
}
?
一旦發生錯誤,寫入方法將變為無效,但是會保存錯誤值。
給出errWrite類型及其寫入方法,可以重構以上代碼:
?
ew:=&errWriter {w:fd}
ew.write(p0 [a:b])
ew.write(p1 [c:d])
ew.write(p2 [e:f])
// 等等
如果ew.err!= nil {
???? 返回ew.err
}
?
這更干凈,甚至與使用閉包相比,也使得實際的寫入序列在頁面上更容易看到。沒有任何雜亂。使用錯誤值(和接口)編程使代碼更好。
?
同一個軟件包中的其他一些代碼很可能會建立在這個想法上,甚至直接使用errWriter。
?
另外,一旦errWriter存在,還有更多的可以幫助,特別是在較少人為的例子中。它可以累加字節數。它可以將寫入合并到單個緩沖區,然后可以原子傳輸。以及更多。
?
實際上,這種模式經常出現在標準庫中。Archive/zip和net/http包使用它。這個討論更為突出,bufio包的Writer實際上是一個errWriter想法的實現。雖然bufio.Writer.Write返回一個error,這主要是關于遵守io.Writer接口的形式。bufio.Writer的Write方法與上面的errWrite.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()
}
?
這種方法有一個明顯的缺點,至少對于某些應用程序:無法知道錯誤發生之前完成了多少處理。如果這些信息很重要,則需要采用更細粒度的方法。通常,盡管如此,一個在末尾的完全或沒有檢查就足夠了。
?
我們只看了一種避免重復錯誤處理代碼的技術。請記住,使用errWrite或者bufio.Writer不是簡化錯誤處理的唯一方法,而且這種方法不適用于所有情況。然而,重要的教訓是errors是值,Go編程語言的全部功能可用于處理它們。
使用語言來簡化錯誤處理。
?
但記住:無論你做什么,總是檢查你的errors。
?
最后,關于我與@jxck_互動的完整故事,包括他錄制的一點視頻,請訪問他的博客。?
?
轉載于:https://www.cnblogs.com/majianguo/p/6793152.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Errors are values的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: likelihood(似然) and l
- 下一篇: 第一阶段个人总结03