譯|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
方法呼叫底層 Writer
的 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/zip
和 net/http
包在使用。該討論最顯著的是, bufio
包的 Writer
實際上是 errWriter
想法的實現。 儘管 bufio.Writer.Write
返回錯誤,但主要是在於實現 io.Writer
介面。 bufio.Writer
的 Write
方法就像我們上面的 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
並不是簡化錯誤處理的唯一方法,並且這種方法並不適合所有情況。 然而,關鍵的一課是 errors are values
,並且Go程式語言的全部功能可用於處理它們。
使用語言簡化錯誤處理。
但請記住:無論你怎麼做,一定要檢查自己的錯誤!
最後,關於我與 @jxck 互動的完整故事,包括他錄製的一個小視訊,請訪問他的部落格 。