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