社區(qū)精選 | if err != nil 太煩?Go 創(chuàng)始人教你如何對(duì)錯(cuò)誤進(jìn)行編程!
今天小編為大家?guī)?lái)的是社區(qū)作者 煎魚(yú) 的文章,在這篇文章中他將和大家一起學(xué)習(xí)如何對(duì)錯(cuò)誤進(jìn)行編程!
大家好,我是煎魚(yú)。
前段時(shí)間我分享了一篇文章《10+ 條 Go 官方諺語(yǔ),你知道幾條?》,引發(fā)了許多小伙伴的討論。其中有一條 “Errors are values”,大家在是 “錯(cuò)誤是值” 還是 “錯(cuò)誤就是價(jià)值” 中反復(fù)橫跳,糾結(jié)不易。
其實(shí)說(shuō)這句話(huà)的 Rob Pike,他用一篇文章《Errors are values》詮釋了這句諺語(yǔ)的意思,到底是什么?
文章鏈接:https://go.dev/blog/errors-are-values
今天煎魚(yú)和大家一起學(xué)習(xí),以下的 “我” 均代表 Rob Pike。
背景
Go 程序員,尤其是那些剛接觸該語(yǔ)言的程序員,經(jīng)常討論的一個(gè)問(wèn)題是如何處理錯(cuò)誤。對(duì)于以下代碼片段出現(xiàn)的次數(shù),談話(huà)經(jīng)常變成哀嘆(各大平臺(tái)吐槽、批判非常多,認(rèn)為設(shè)計(jì)的不好)。
如下代碼:
if err != nil {
return err
}
掃描代碼片段
我們最近掃描了我們能找到的所有 Go 開(kāi)源項(xiàng)目,發(fā)現(xiàn)這個(gè)代碼片段只在每一兩頁(yè)出現(xiàn)一次,比一些人認(rèn)為的要少。
盡管如此,如果人們?nèi)匀徽J(rèn)為必須經(jīng)常輸入如下代碼:
if err != nil
那么一定有什么地方出了問(wèn)題,而明顯的目標(biāo)就是 Go 語(yǔ)言本身(說(shuō)設(shè)計(jì)的不好?)。
錯(cuò)誤的理解
顯然這是不幸的,誤導(dǎo)的,而且很容易糾正。也許現(xiàn)在的情況是,剛接觸 Go 的程序員會(huì)問(wèn):"如何處理錯(cuò)誤?",學(xué)習(xí)這種模式,然后就此打住。
在其他語(yǔ)言中,人們可能會(huì)使用 try-catch 塊或其他類(lèi)似機(jī)制來(lái)處理錯(cuò)誤。因此,程序員認(rèn)為,當(dāng)我在以前的語(yǔ)言中會(huì)使用 try-catch 時(shí),我在 Go 中只需輸入 if err != nil。
隨著時(shí)間的推移,Go 代碼中收集了許多這樣的片段,結(jié)果感覺(jué)很笨拙。
錯(cuò)誤是值
不管這種解釋是否合適,很明顯,這些 Go 程序員錯(cuò)過(guò)了關(guān)于錯(cuò)誤的一個(gè)基本點(diǎn):錯(cuò)誤是值(Errors are values)。
值可以被編程,既然錯(cuò)誤是值,那么錯(cuò)誤也可以被編程。
當(dāng)然,涉及錯(cuò)誤值的常見(jiàn)語(yǔ)句是測(cè)試它是否為 nil,但是還有無(wú)數(shù)其他事情可以用錯(cuò)誤值做,并且應(yīng)用其中一些其他事情可以使您的程序更好,消除很多樣板。
如果使用死記硬背的 if 語(yǔ)句檢查每個(gè)錯(cuò)誤,就會(huì)出現(xiàn)這種情況(也就是 if err != nil 到處都是的情況)。
bufio 例子
下面是一個(gè)來(lái)自 bufio 包的 Scanner 類(lèi)型的簡(jiǎn)單例子。它的 Scan 方法執(zhí)行了底層的 I/O,這當(dāng)然會(huì)導(dǎo)致一個(gè)錯(cuò)誤。然而,Scan 方法根本沒(méi)有暴露出錯(cuò)誤。
相反,它返回一個(gè)布爾值,并在掃描結(jié)束時(shí)運(yùn)行一個(gè)單獨(dú)的方法,報(bào)告是否發(fā)生錯(cuò)誤。
客戶(hù)端代碼看起來(lái)像這樣:
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
當(dāng)然,有一個(gè) nil 檢查錯(cuò)誤,但它只出現(xiàn)并執(zhí)行一次。Scan 方法可以改為定義為:
func (s *Scanner) Scan() (token []byte, error)
然后,用戶(hù)代碼的例子可能是(取決于如何檢索令牌):
scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
這并沒(méi)有太大的不同,但有一個(gè)重要的區(qū)別。在這段代碼中,客戶(hù)端必須在每次迭代時(shí)檢查錯(cuò)誤,但在真正的 Scanner API 中,錯(cuò)誤處理是從關(guān)鍵 API 元素中抽象出來(lái)的,它正在迭代令牌。
使用真正的 API,客戶(hù)端的代碼因此感覺(jué)更自然:循環(huán)直到完成,然后擔(dān)心錯(cuò)誤。
錯(cuò)誤處理不會(huì)掩蓋控制流程。
當(dāng)然,在幕后發(fā)生的事情是,一旦 Scan 遇到 I/O 錯(cuò)誤,它就會(huì)記錄它并返回 false。當(dāng)客戶(hù)端詢(xún)問(wèn)時(shí),一個(gè)單獨(dú)的方法 Err 會(huì)報(bào)告錯(cuò)誤值。
雖然這很微不足道,但它與在每個(gè) if err != nil 后到處放或要求客戶(hù)端檢查錯(cuò)誤是不一樣的。這是用錯(cuò)誤值編程。簡(jiǎn)單的編程,是的,但仍然是編程。
值得強(qiáng)調(diào)的是,無(wú)論設(shè)計(jì)如何,程序檢查錯(cuò)誤是至關(guān)重要的,無(wú)論它們暴露在哪里。這里的討論不是關(guān)于如何避免檢查錯(cuò)誤,而是關(guān)于使用語(yǔ)言?xún)?yōu)雅地處理錯(cuò)誤。
實(shí)戰(zhàn)探討
當(dāng)我參加在東京舉行的 2014 年秋季 GoCon 時(shí),出現(xiàn)了重復(fù)錯(cuò)誤檢查代碼的話(huà)題。一位熱心的 Gopher,在 Twitter 上的名字是 @jxck\_,回應(yīng)了我們熟悉的關(guān)于錯(cuò)誤檢查的哀嘆。
他有一些代碼,從結(jié)構(gòu)上看是這樣的:
_, 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
它是非常重復(fù)的。在真正的代碼中,這段代碼比較長(zhǎng),有更多的事情要做,所以不容易只是用一個(gè)輔助函數(shù)來(lái)重構(gòu)這段代碼,但在這種理想化的形式中,一個(gè)函數(shù)字面的關(guān)閉對(duì)錯(cuò)誤變量會(huì)有幫助:
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
}
這種模式效果很好,但需要在每個(gè)執(zhí)行寫(xiě)入的函數(shù)中關(guān)閉;單獨(dú)的輔助函數(shù)使用起來(lái)比較笨拙,因?yàn)樾枰谡{(diào)用之間維護(hù) err 變量(嘗試一下)。
我們可以通過(guò)借用上面的掃描方法的思路,使之更簡(jiǎn)潔、更通用、更可重復(fù)使用。我在我們的討論中提到了這個(gè)技術(shù),但是 @jxck\_ 沒(méi)有看到如何應(yīng)用它。經(jīng)過(guò)長(zhǎng)時(shí)間的交流,在語(yǔ)言不通的情況下,我問(wèn)能不能借他的筆記本,打一些代碼給他看。
我定義了一個(gè)名為 errWriter 的對(duì)象,如下所示:
type errWriter struct {
w io.Writer
err error
}
并給了它一種方法,Write。它不需要具有標(biāo)準(zhǔn)的 Write 簽名,并且部分小寫(xiě)以突出區(qū)別。write 方法調(diào)用底層 Writer 的 Write 方法,并記錄第一個(gè)錯(cuò)誤以備參考:
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
一旦發(fā)生錯(cuò)誤,Write 方法就會(huì)變成無(wú)用功,但錯(cuò)誤值會(huì)被保存。
鑒于 errWriter 類(lèi)型和它的 Write 方法,上面的代碼可以被重構(gòu)為如下代碼:
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
}
這更干凈,甚至與使用閉包相比,也使實(shí)際的寫(xiě)入順序更容易在頁(yè)面上看到。不再有混亂。使用錯(cuò)誤值(和接口)進(jìn)行編程使代碼更好。
很可能同一個(gè)包中的其他一些代碼可以基于這個(gè)想法,甚至直接使用 errWriter。
另外,一旦 errWriter 存在,它可以做更多的事情來(lái)幫助,特別是在不太人性化的例子中。它可以積累字節(jié)數(shù)。它可以將寫(xiě)內(nèi)容凝聚成一個(gè)緩沖區(qū),然后以原子方式傳輸。還有更多。
事實(shí)上,這種模式經(jīng)常出現(xiàn)在標(biāo)準(zhǔn)庫(kù)中。archive/zip 和 net/http 包使用它。在這個(gè)討論中更突出的是,bufio 包的 Writer 實(shí)際上是 errWriter 思想的一個(gè)實(shí)現(xiàn)。盡管 bufio.Writer.Write 返回錯(cuò)誤,但這主要是為了尊重 io.Writer 接口。
bufio.Writer 的 Write 方法的行為就像我們上面的 errWriter.write 方法一樣,F(xiàn)lush 會(huì)報(bào)錯(cuò),所以我們的例子可以這樣寫(xiě):
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()
}
這種方法有一個(gè)明顯的缺點(diǎn),至少對(duì)于某些應(yīng)用程序而言:沒(méi)有辦法知道在錯(cuò)誤發(fā)生之前完成了多少處理。如果該信息很重要,則需要更細(xì)粒度的方法。不過(guò),通常情況下,最后進(jìn)行全有或全無(wú)檢查就足夠了。
總結(jié)
在本文中我們只研究了一種避免重復(fù)錯(cuò)誤處理代碼的技術(shù)。
請(qǐng)記住,使用 errWriter 或 bufio.Writer 并不是簡(jiǎn)化錯(cuò)誤處理的唯一方法,而且這種方法并不適用于所有情況。
然而,關(guān)鍵的教訓(xùn)是錯(cuò)誤是值,Go 編程語(yǔ)言的全部功能可用于處理它們。
使用該語(yǔ)言來(lái)簡(jiǎn)化您的錯(cuò)誤處理。
但請(qǐng)記住:無(wú)論您做什么,都要檢查您的錯(cuò)誤!
Go 圖書(shū)系列
Go 語(yǔ)言入門(mén)系列:初探 Go 項(xiàng)目實(shí)戰(zhàn) https://eddycjy.com/go-categories/ Go 語(yǔ)言編程之旅:深入用 Go 做項(xiàng)目 https://golang2.eddycjy.com/ Go 語(yǔ)言設(shè)計(jì)哲學(xué):了解 Go 的為什么和設(shè)計(jì)思考 https://golang3.eddycjy.com/ Go 語(yǔ)言進(jìn)階之旅:進(jìn)一步深入 Go 源碼 https://golang1.eddycjy.com/
更多閱讀
Go 想要加個(gè)箭頭語(yǔ)法,這下更像 PHP 了! https://mp.weixin.qq.com/s/uo23gKC_Lbm0JNe5_YbVfA Go 錯(cuò)誤處理新思路?用左側(cè)函數(shù)和表達(dá)式 https://mp.weixin.qq.com/s/nzoFI8ANBVDP9VPWfDgoHw

