來源: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
如下是 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 互動的完整故事,包括他錄製的一個小視頻,請訪問他的博客 。