關(guān)于Go錯(cuò)誤處理新提案的一個(gè)想法:?操作符這樣用行不行
0. 背景
Ian Taylor在關(guān)閉了旨在消除Go錯(cuò)誤處理樣板代碼的issue[1]之后,又另起了一個(gè)“同名”的discussion[2]。錯(cuò)誤處理真不愧是Go社區(qū)呼聲最高的問題,幾天之內(nèi)又收到了近500條回復(fù)!不過到目前為止,依然沒有形成統(tǒng)一和高贊的意見。
關(guān)于error handling的樣板代碼過多,其實(shí)我個(gè)人是可以接受的,即便不做出任何改變也是ok的,估計(jì)Go社區(qū)與我有相同看法的也不在少數(shù)。比如就有人引用了Rob Pike的權(quán)威觀點(diǎn)[3],并認(rèn)為Go應(yīng)該按照Rob大神的思路,保持Go語法穩(wěn)定:
不過自然也會有另外一批人強(qiáng)烈希望錯(cuò)誤處理的樣板代碼得到改進(jìn)。
Ian Taylor在discussion中明確了該提案的目標(biāo)是引入一種新語法,在不影響控制流清晰度的前提下,減少正常情況下檢查錯(cuò)誤所需的代碼量。
Ian最初的Proposal由于隱式聲明變量err以及可選代碼塊等問題而備受“批評”,并且似乎該proposal違反了他自己提出的目標(biāo)。
今天在discussion中看到一位名為Mukunda Johnson的gopher的評論[4],我覺得很有道理。其核心觀點(diǎn)就是:盡量保持Go的傳統(tǒng)語法形式。他還給出了期望中的語法示例:
// 當(dāng)前錯(cuò)誤處理樣板代碼過多的示例
f, err := open(file)
if err != nil {
return err
}
defer f.Close()
if err = binwrite(f, signature); err != nil {
return err
}
if err = binwrite(f, header); err != nil {
return err
}
if err = binwrite(f, zeroSegment); err != nil {
return err
}
for _, s := range segments {
if err = binwrite(f, s); err != nil {
return err
}
}
if err = binwrite(f, footer); err != nil {
return err
}
vs.
// 使用新語法改進(jìn)后的代碼
f, err := open(file)?
defer f.Close()
binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?
for _, s := range segments {
binwrite(f, s)?
}
binwrite(f, footer)?
這給了我很大啟發(fā):我們可以引入?語法,但是如果結(jié)合原先err變量的聲明形式豈不是更好!比如:
f, err := open(file)?
豈不是要比下面兩種形式更好!
f := open(file)?
或
f := open(file)? err { }
通過僅引入一個(gè)問號(?)操作符,并避免引入過多的新語法形式,卻能解決60%的錯(cuò)誤處理樣板問題。根據(jù)jba對Go開源代碼中錯(cuò)誤處理的抽樣統(tǒng)計(jì)[5],超過60%的錯(cuò)誤處理都是直接返回err,而沒有對err進(jìn)行任何修飾。此外,顯式聲明err可以最大程度地避免隱式聲明帶來的問題,同時(shí)提升代碼的可讀性。
因此,基于盡量使用已有Go代碼風(fēng)格、最大程度避免隱式聲明,并僅解決最常見的錯(cuò)誤處理樣板代碼的原則,下面我基于Ian提案的錯(cuò)誤處理改進(jìn)語法,談點(diǎn)自己關(guān)于新?操作符使用的想法,大家看看是否可行。
1. 對于最常見的未經(jīng)修飾的錯(cuò)誤處理代碼
err := SomeFunction2()
if err != nil {
return err
}
或是
if err := SomeFunction2(); err != nil {
return err
}
我們使用下面的新語法做等價(jià)替代:
err := SomeFunction2() ?
如果聲明的錯(cuò)誤變量名為err,也可省略賦值操作符左側(cè)代碼,從而簡化為:
SomeFunction2() ? // 這里略帶隱式
2. 如果函數(shù)返回值有多個(gè),甚至有多個(gè)錯(cuò)誤變量的情況
比如下面代碼:
a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
return err2
}
我們可以將其改寫為:
a, b, err0, err1, err2 := SomeFunction3()?
其語義是如果err2不為nil,返回err2,但前提要保證賦值語句的左側(cè)的最后一個(gè)變量err2必須是實(shí)現(xiàn)error接口的類型的變量。
如果是像下面這樣在err2 != nil時(shí)有多個(gè)返回值,又該如何處理呢?
a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
return a, b, err2
}
對于這種情況,我認(rèn)為可以不在新方案的考慮范圍之內(nèi),現(xiàn)在怎么寫,請繼續(xù)這么寫。如果非要解決,請繼續(xù)看后面支持可選代碼塊的情況。
實(shí)現(xiàn)以上兩種情況,就能解決60%以上的錯(cuò)誤樣板代碼問題了!
3. 對于對返回的error值進(jìn)行修飾的情況
對于像下面兩種對返回的error變量進(jìn)行修飾的情況:
r, err := SomeFunction()
if err != nil {
return fmt.Errorf("something failed: %v", err)
}
和
if err := SomeFunction2(); err != nil {
return fmt.Errorf("something else failed: %v", err)
}
我的第一想法是保持現(xiàn)狀 ,不在新方案考慮范圍之內(nèi)。
不過如果非要在新方案中解決,那就需要引入可選代碼塊(optional block)了!比如:
r, err := SomeFunction() ? {
return fmt.Errorf("something failed: %v", err)
}
err := SomeFunction2() ? {
return fmt.Errorf("something else failed: %v", err)
}
和Ian的原proposal中語法不同,這里我們依然顯式聲明了err,當(dāng)然你也可以不用err這個(gè)名字,由于是顯式聲明,你用任何名字均可,比如:
r, e := SomeFunction() ? {
return fmt.Errorf("something failed: %v", e)
}
myErr := SomeFunction2() ? {
return fmt.Errorf("something else failed: %v", myErr)
}
這將避免隱式聲明帶來的諸多問題!
基于可選代碼塊,我們也可以處理一下前面提到的返回多個(gè)值的情況。下面代碼
a, b, err0, err1, err2 := SomeFunction3()
if err2 != nil {
return a, b, err2
}
可以改寫為:
a, b, err0, err1, err2 := SomeFunction3() ? {
return a, b, err2
}
這里加入可選代碼塊后,我建議開發(fā)人員負(fù)責(zé)顯式調(diào)用return,而不是由?操作符來自動return,也就是說完全將控制權(quán)交給你。如果你沒有在可選代碼塊中調(diào)用return,那么代碼在執(zhí)行完可選代碼塊中的代碼后,還會繼續(xù)向下執(zhí)行??蛇x代碼塊相當(dāng)于一個(gè)error handler,而不帶可選代碼塊的情況,默認(rèn)的error handler其實(shí)就是一個(gè)return err,偽代碼類似這樣:
err := SomeFunction2() ?
<=>
err := SomeFunction2() ? {
return err
}
這樣解釋后,你是不是覺得在語義層面,不帶可選代碼塊與帶有可選代碼塊的情況就統(tǒng)一和一致了呢!
本質(zhì)上來說,?+可選代碼塊僅是讓你少敲了個(gè)if以及err != nil。
4. 綜合示例
Mukunda Johnson給出的示例其實(shí)已經(jīng)可以很好地展示?操作符+顯式聲明err方案帶來的消除樣板代碼的效果,這里再回顧一下(這里沒用到可選代碼塊,因此代碼顯得格外清晰):
f, err := open(file)?
defer f.Close()
binwrite(f, signature)?
binwrite(f, header)?
binwrite(f, zeroSegment)?
for _, s := range segments {
binwrite(f, s)?
}
binwrite(f, footer)?
此外,在原discussion中,另外一個(gè)gopher提出的示例,我們也可以用上面的想法改寫一下:
// 最常見的情況
SomeFunc() ?
// 多個(gè)返回值,最后一個(gè)為error變量
a, err1 := SomeFunction2() ?
// 返回前對err進(jìn)行修飾
err := SomeFunc() ? {
return fmt.Errorf("oh no: %w", err)
}
// 顯式聲明避免變量遮蔽
err := SomeFunc() ? {
otherErr := OtherFunc() ? {
err = errors.Wrap(err, otherErr) // 在可選代碼塊中沒有顯式調(diào)用return,代碼還會繼續(xù)向后執(zhí)行
}
return fmt.Errorf("oh no: %w", err)
}
5. 小結(jié)
再來簡單總結(jié)一下上面想法中的語法形式的優(yōu)勢:
- 與傳統(tǒng)Go語法形式幾乎一致,盡量避免引入過多新語法形式,在不使用可選代碼塊的時(shí)候,只是多了一個(gè)問號(?)。
- 顯式聲明err變量,最大程度避免隱式聲明帶來的問題。
- 專注解決最常見的錯(cuò)誤處理樣板情景,其他場景保持當(dāng)前寫法即可。
- 即便引入可選代碼塊,本質(zhì)上與不用可選代碼塊的語法在語義層面也是統(tǒng)一和一致的。
這一語法方案保留了原Ian提案中的優(yōu)勢,并能消除一些缺點(diǎn),如變量遮蔽和隱式聲明等。不過,仍然有些原proposal中的劣勢問題無法完全消除,但這些問題顯然不是主要關(guān)注點(diǎn)。
需要注意的是,以上想法目前僅停留在形式討論層面,技術(shù)層面是否可行尚不確定。
大家認(rèn)為我的想法可行嗎?希望大家能提出更具建設(shè)性的意見^_^。
參考資料
[1] 旨在消除Go錯(cuò)誤處理樣板代碼的issue: https://github.com/golang/go/issues/71203
[2] discussion: https://github.com/golang/go/discussions/71460
[3] Rob Pike的權(quán)威觀點(diǎn): https://go.dev/talks/2015/simplicity-is-complicated.slide#9
[4] Mukunda Johnson的gopher的評論: https://github.com/golang/go/discussions/71460#discussioncomment-12084482
[5] jba對Go開源代碼中錯(cuò)誤處理的抽樣統(tǒng)計(jì): https://github.com/golang/go/issues/71203#issuecomment-2585915103