Go 錯(cuò)誤處理語(yǔ)法之爭(zhēng)塵埃落定?Go 團(tuán)隊(duì)為何十五年探索后仍選擇“不”
本文轉(zhuǎn)載自微信公眾號(hào)「TonyBai」,作者白明的贊賞賬戶 。轉(zhuǎn)載本文請(qǐng)聯(lián)系TonyBai公眾號(hào)。
長(zhǎng)久以來(lái),Go 語(yǔ)言中 if err != nil 的錯(cuò)誤處理模式因其普遍性和由此帶來(lái)的代碼冗余,一直是社區(qū)反饋中最持久、最突出的痛點(diǎn)之一。盡管 Go 團(tuán)隊(duì)及社區(qū)投入了大量精力,歷經(jīng)近十五年的探索,提出了包括 check/handle、try 內(nèi)建函數(shù)以及借鑒 Rust的?操作符在內(nèi)的多種方案,但始終未能就新的錯(cuò)誤處理語(yǔ)法達(dá)成廣泛共識(shí)。近日,Go 官方團(tuán)隊(duì)通過(guò)一篇博文(https://go.dev/blog/error-syntax)正式闡述了其最新立場(chǎng):在可預(yù)見的未來(lái),將停止尋求通過(guò)改變語(yǔ)法來(lái)簡(jiǎn)化錯(cuò)誤處理,并將關(guān)閉所有相關(guān)的提案。 這一決策無(wú)疑在 Go 社區(qū)引發(fā)了廣泛關(guān)注和深入思考。
漫漫探索路:從 check/handle 到 ? 操作符
Go 語(yǔ)言的錯(cuò)誤處理冗余問(wèn)題,尤其在涉及大量 API 調(diào)用且錯(cuò)誤處理邏輯相對(duì)簡(jiǎn)單的場(chǎng)景下尤為突出。一個(gè)典型的例子如下:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err // 樣板代碼
}
y, err := strconv.Atoi(b)
if err != nil {
return err // 樣板代碼
}
fmt.Println("result:", x + y)
return nil
}
在這個(gè)函數(shù)中,近一半的代碼行用于錯(cuò)誤檢查和返回,這無(wú)疑增加了代碼的視覺噪音,降低了核心邏輯的清晰度。因此,多年來(lái),改進(jìn)錯(cuò)誤處理的呼聲在 Go 開發(fā)者年度調(diào)查中一直居高不下。
Go 團(tuán)隊(duì)對(duì)此高度重視,并進(jìn)行了一系列嘗試:
check/handle 機(jī)制 (2018年)
由 Russ Cox 正式提出,基于 Marcel van Lohuizen 的草案設(shè)計(jì)。該機(jī)制引入了 check 用于檢查錯(cuò)誤并提前返回,handle 用于定義錯(cuò)誤處理邏輯。
// 設(shè)想的 check/handle 用法
func printSum(a, b string) error {
handle err { return err } // 定義錯(cuò)誤處理
x := check strconv.Atoi(a) // 檢查錯(cuò)誤
y := check strconv.Atoi(b) // 檢查錯(cuò)誤
fmt.Println("result:", x + y)
return nil
}
然而,該方案因其復(fù)雜性未被廣泛接受。
try 內(nèi)建函數(shù) (2019年)
作為 check/handle 的簡(jiǎn)化版,try 函數(shù)會(huì)在遇到錯(cuò)誤時(shí)從其所在的封閉函數(shù)返回。
// 設(shè)想的 try 用法
func printSum(a, b string) error {
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
盡管 Go 團(tuán)隊(duì)投入巨大,但 try 因其隱式的控制流改變(可能從深層嵌套表達(dá)式中返回)而遭到許多開發(fā)者的反對(duì),最終也被放棄。Go 團(tuán)隊(duì)反思,或許引入新關(guān)鍵字并限制 try 的使用范圍會(huì)是更好的路徑。
借鑒 Rust 的 ? 操作符 (2024年)
由 Ian Lance Taylor 提出,希望通過(guò)借鑒其他語(yǔ)言中已驗(yàn)證的機(jī)制來(lái)取得突破。
// 設(shè)想的 ? 操作符用法
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}
此方案雖然在小范圍用戶研究中表現(xiàn)出一定的直觀性,但在社區(qū)討論中依然未能形成足夠支持,并引發(fā)了大量關(guān)于細(xì)節(jié)調(diào)整的建議。
除了官方提案,社區(qū)也貢獻(xiàn)了數(shù)以百計(jì)的錯(cuò)誤處理改進(jìn)方案,但無(wú)一例外都未能獲得壓倒性的支持。
官方立場(chǎng):為何按下暫停鍵?
面對(duì)多年探索未果的局面,Go 團(tuán)隊(duì)基于以下幾點(diǎn)理由,做出了暫停錯(cuò)誤處理語(yǔ)法層面改進(jìn)的決定。
缺乏社區(qū)共識(shí)
這是最核心的原因。根據(jù) Go 的提案流程,一項(xiàng)提案需要得到社區(qū)的普遍共識(shí)才能被接受。然而,在錯(cuò)誤處理語(yǔ)法這個(gè)問(wèn)題上,無(wú)論是官方還是社區(qū)的提案,都未能凝聚起足夠的共識(shí)。甚至 Go 團(tuán)隊(duì)內(nèi)部也未能就最佳方案達(dá)成一致。
維護(hù)現(xiàn)狀的合理性
- 時(shí)機(jī)問(wèn)題: Go 已經(jīng)發(fā)展了十五年,現(xiàn)有的錯(cuò)誤處理方式雖然冗余,但功能完善且被廣泛理解和使用。早期引入語(yǔ)法糖可能更容易被接受,但現(xiàn)在改變的門檻更高。
- 避免制造新的“不快樂(lè)”: 即使找到了“完美”方案,強(qiáng)制推廣新語(yǔ)法也可能讓習(xí)慣了現(xiàn)有方式的開發(fā)者感到不適,重蹈類似泛型引入初期的一些爭(zhēng)議。但與泛型不同,錯(cuò)誤處理語(yǔ)法幾乎會(huì)影響所有開發(fā)者。
- Go 的設(shè)計(jì)哲學(xué): Go 傾向于“只提供一種(或盡可能少)的方式來(lái)做同一件事”。引入新的錯(cuò)誤處理語(yǔ)法會(huì)打破這一原則。有趣的是,:= 短變量聲明中的變量重聲明規(guī)則,最初也是為了解決連續(xù)錯(cuò)誤檢查中 err 變量命名問(wèn)題而引入的,如果早期有更好的錯(cuò)誤處理語(yǔ)法,這個(gè)規(guī)則或許就不需要了。
關(guān)注錯(cuò)誤處理的本質(zhì),而非僅僅語(yǔ)法
- 當(dāng)錯(cuò)誤被“真正處理”時(shí),冗余感會(huì)降低。 良好的錯(cuò)誤處理通常需要附加額外上下文信息,而不僅僅是簡(jiǎn)單返回。例如:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("invalid integer: %q", a) // 附加信息
}
// ...
return nil
}
在這種情況下,if err != nil 的樣板代碼占比相對(duì)減小。
- 標(biāo)準(zhǔn)庫(kù)的增強(qiáng): 新的庫(kù)函數(shù)(如 cmp.Or)或未來(lái)的庫(kù)特性,可以在不改變語(yǔ)法的情況下幫助減少錯(cuò)誤處理的樣板代碼。
func printSum(a, b string) error {
x, err1 := strconv.Atoi(a)
y, err2 := strconv.Atoi(b)
if err := cmp.Or(err1, err2); err != nil { // 使用 cmp.Or
return err
}
fmt.Println("result:", x+y)
return nil
}
工具的輔助作用
- 編寫時(shí): 現(xiàn)代 IDE(包括基于 LLM 的工具)已經(jīng)能夠很好地輔助生成重復(fù)的錯(cuò)誤檢查代碼。
- 閱讀時(shí): IDE 或可提供隱藏/折疊錯(cuò)誤處理代碼塊的功能,減少視覺干擾。
- 調(diào)試時(shí): 顯式的 if 語(yǔ)句更便于設(shè)置斷點(diǎn)和添加調(diào)試輸出,而高度集成的語(yǔ)法糖可能會(huì)使調(diào)試變得復(fù)雜。
語(yǔ)言演進(jìn)的成本與優(yōu)先級(jí)
- 任何語(yǔ)言的改動(dòng)都伴隨著巨大的成本:設(shè)計(jì)、實(shí)現(xiàn)、文檔更新、工具調(diào)整以及社區(qū)的適應(yīng)。Go 團(tuán)隊(duì)規(guī)模有限,需要優(yōu)先處理其他重要事項(xiàng)。
- 開發(fā)者習(xí)慣的演變: 許多有經(jīng)驗(yàn)的 Go 開發(fā)者表示,隨著對(duì) Go 錯(cuò)誤處理哲學(xué)的深入理解和實(shí)踐,最初感到的冗余問(wèn)題會(huì)逐漸減輕。
對(duì)開發(fā)者的影響與未來(lái)展望
Go 團(tuán)隊(duì)的這一決定,意味著在可預(yù)見的未來(lái),if err != nil 仍將是 Go 語(yǔ)言錯(cuò)誤處理的標(biāo)準(zhǔn)范式。開發(fā)者需要:
- 接受現(xiàn)狀并深入理解其哲學(xué): Rob Pike 的名言“Errors are values”依然是理解 Go 錯(cuò)誤處理的核心。錯(cuò)誤是程序正常流程的一部分,顯式處理它們有助于編寫健壯的軟件。
- 利用現(xiàn)有工具和庫(kù):
善用 IDE 的代碼生成和輔助功能。
探索和使用標(biāo)準(zhǔn)庫(kù)或第三方庫(kù)提供的錯(cuò)誤處理輔助工具(如 errors.Is, errors.As, fmt.Errorf的 %w 以及可能的新庫(kù)特性)。
- 關(guān)注代碼質(zhì)量而非單純追求簡(jiǎn)潔: 在需要詳細(xì)錯(cuò)誤上下文的地方,不要吝嗇代碼。清晰、可追溯的錯(cuò)誤比極度簡(jiǎn)化的語(yǔ)法糖更有價(jià)值。
- 代碼可讀性依然重要: 盡管語(yǔ)法層面不再追求極致簡(jiǎn)潔,但在錯(cuò)誤處理邏輯本身,依然要力求清晰、易懂。
Go 團(tuán)隊(duì)也指出,他們并未完全關(guān)閉對(duì)錯(cuò)誤處理改進(jìn)的大門,只是將焦點(diǎn)從“語(yǔ)法層面”移開。未來(lái)可能會(huì)更深入地研究錯(cuò)誤處理的本質(zhì)問(wèn)題,例如如何更好地構(gòu)造和傳遞包含豐富上下文的錯(cuò)誤信息,以及通過(guò)庫(kù)而非語(yǔ)法來(lái)提供更好的支持。
小結(jié)
Go 語(yǔ)言在錯(cuò)誤處理語(yǔ)法上的探索歷程,充分體現(xiàn)了其在語(yǔ)言設(shè)計(jì)上的審慎與對(duì)社區(qū)反饋的重視。盡管長(zhǎng)達(dá)十五年的努力未能催生出被廣泛接受的新語(yǔ)法,但這并不代表失敗,而是對(duì) Go 核心設(shè)計(jì)原則的堅(jiān)守和對(duì)現(xiàn)實(shí)復(fù)雜性的認(rèn)知。
對(duì)開發(fā)者而言,這意味著需要繼續(xù)在現(xiàn)有的、經(jīng)過(guò)驗(yàn)證的錯(cuò)誤處理模式下精進(jìn)技藝,同時(shí)期待 Go 語(yǔ)言在庫(kù)和工具層面帶來(lái)更多輔助,以更優(yōu)雅、更高效地構(gòu)建可靠的應(yīng)用程序。
這場(chǎng)關(guān)于錯(cuò)誤處理的“語(yǔ)法之爭(zhēng)”雖然暫時(shí)告一段落,但其引發(fā)的關(guān)于簡(jiǎn)潔、清晰、實(shí)用與語(yǔ)言穩(wěn)定性的思考,將對(duì) Go 的長(zhǎng)遠(yuǎn)發(fā)展產(chǎn)生深遠(yuǎn)影響。