關于 Golang 的錯誤處理的討論可以大結局了
關于 Go 語言最有爭論的就是錯誤處理:
x, err := call()
if err != nil {
// handle err
}if err != nil 類似于這樣的代碼非常多,淹沒了其余真正有用的代碼。這通常發生在進行大量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團隊認真對待社區反饋,因此多年來我們一直在嘗試為這個問題找到解決方案,并聽取 Go 社區的意見。
Go 團隊的第一次明確嘗試可以追溯到 2018 年,當時Russ Cox正式提到了這個問題[2],作為我們當時稱為 Go2 努力的一部分。他基于 Marcel van Lohuizen 的草案設計[3]概述了一個可能的解決方案。該設計基于check和handle機制,相當全面。草案包括對替代解決方案的詳細分析,包括與其他語言采用的方法的比較。如果您想知道您的特定錯誤處理想法之前是否被考慮過,請閱讀這份文檔!
// printSum implementation using the proposed check/handle mechanism.
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
}check和handle方法被認為過于復雜,大約一年后,在2019年,我們推出了更加簡化的、現在臭名昭著[4]的try提案[5]。它基于 check 和 handle 的思想,但 check 偽關鍵字變成了try內置函數,handle部分被省略了。為了探索try內置函數的影響,我們編寫了一個簡單的工具(tryhard[6]),使用try重寫現有的錯誤處理代碼。這個提案被激烈爭論,在GitHub問題[7]上接近900條評論。
// printSum implementation using the proposed try mechanism.
func printSum(a, b string) error {
// use a defer statement to augment errors before returning
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}然而,try通過在出錯時從封閉函數返回來影響控制流,并且可能從深度嵌套的表達式中這樣做,從而隱藏了這種控制流。這使得該提案對許多人來說難以接受,盡管在這個提案上投入了大量精力,我們還是決定放棄這項工作。回顧起來,引入一個新關鍵字可能會更好,這是我們現在可以做的事情,因為我們通過go.mod文件和特定文件的指令對語言版本有細粒度的控制。將try的使用限制在賦值和語句中可能會緩解一些其他的擔憂。Jimmy Frasche的最近提案[8]基本上回到了原始的check和handle設計,并解決了該設計的一些缺點,正朝著這個方向發展。
try提案的反響導致了大量的反思,包括Russ Cox的一系列博客文章:"關于Go提案流程的思考"[9]。其中一個結論是,我們可能通過提出一個幾乎完全成熟的提案,給社區反饋留下很少的空間,以及一個"具有威脅性"的實現時間表,從而降低了獲得更好結果的機會。根據"Go提案流程:大型變更"[10]:"回顧起來,try是一個足夠大的變更,我們發布的新設計應該是第二版草案設計,而不是帶有實現時間表的提案"。但不管在這種情況下可能存在的流程和溝通失敗,用戶對該提案有著非常強烈地抵觸情緒。
當時我們沒有更好的解決方案,幾年來都沒有為錯誤處理追求語法變更。不過,社區中的許多人受到了啟發,我們收到了源源不斷的錯誤處理提案,其中許多非常相似,有些有趣,有些難以理解,有些不可行。為了跟蹤不斷擴大的提案,一年后,Ian Lance Taylor 創建了一個總體問題[11],總結了改進錯誤處理的提議變更的當前狀態。創建了一個Go Wiki[12]來收集相關的反饋、討論和文章。
關于錯誤處理冗長性的抱怨持續存在(參見2024年上半年Go開發者調查結果[13]),因此,在Go團隊內部提案經過一系列日益完善之后,Ian Lance Taylor 在2024年發布了"使用?減少錯誤處理樣板代碼"[14]。這次的想法是借鑒Rust[15]中實現的構造,特別是?操作符[16]。希望通過依靠使用既定符號的現有機制,并考慮我們多年來學到的東西,我們應該能夠最終取得一些進展。在一小批用戶調研中,向開發者展示使用 ? 的 Go 代碼時,絕大多數參與者正確猜出了代碼的含義,這進一步說服我們再試一次。為了能夠看到變化的影響,Ian 編寫了一個工具,將普通 Go 代碼轉換為使用提議的新語法的代碼,我們還在編譯器中對該功能進行了原型設計。
// printSum implementation using the proposed "?" statements.
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}不幸的是,與其他錯誤處理想法一樣,這個新提案也很快被評論淹沒,許多人建議進行微調,通常基于個人偏好。Ian關閉了提案,并將內容移到了討論區[17],以促進對話并收集進一步的反饋。一個稍作修改的版本得到了稍微積極一些[18]的接受,但廣泛的支持仍然難以達成一致。
經過這么多年的嘗試,Go團隊提出了三個完整的提案,社區提出了數百個提案,其中大多數是各類提案的變體,所有這些都未能獲得足夠(更不用說壓倒性)的支持,我們現在面臨的問題是:如何繼續?我們是否應該繼續?
我們認為不應該。
更準確地說,我們應該停止嘗試解決_語法問題_,至少在可預見的未來是這樣。提案流程[19]為這個決定提供了理由:
提案流程的目標是及時就結果達成普遍共識。如果提案審查無法在問題跟蹤器上的問題討論中確定普遍共識,通常的結果是提案被拒絕。
沒有一個錯誤處理提案達到任何接近共識的程度,所以它們都被拒絕了。即使是 Google 的 Go 團隊最資深的成員也不一致同意目前最佳的方案(也許在某個時候會改變)。但是沒有具體的共識,我們就無法合理地向前推進。
有支持現狀的有效證據:
? 如果 Go 早期就為錯誤處理引入了特定的語法糖,今天幾乎沒有人會爭論它。但我們已經走過了15年,機會已經過去了,Go 有一種完全合適的錯誤處理方式,即使有時看起來可能很冗長。
? 從另一個角度看,假設我們今天找到了完美的解決方案。將其納入語言只會導致從一個不滿意的用戶群體(支持變更的)轉移到另一個(喜歡現狀的)。當我們決定向語言添加泛型時,我們處于類似的情況,盡管有一個重要的區別是:今天沒有人被迫使用泛型,好的泛型庫的編寫使得用戶可以基本忽略它們是不是泛型,這要歸功于類型推斷。相反,如果向語言添加新的錯誤處理語法構造,幾乎每個人都需要開始使用它,以免他們的代碼變得不符合最新的范式。
? 不添加額外的語法符合 Go 的設計規則之一:不提供多種做同一件事的方式。在" foot traffic "的領域有這個規則的例外:賦值就是一個例子。具有諷刺意味的是,在短變量聲明[20](:=)中重新聲明變量的能力是為了解決因錯誤處理而產生的問題而引入的:沒有重新聲明,錯誤檢查序列需要為每個檢查使用不同名稱的err變量(或額外的單獨變量聲明)。當時更好的解決方案可能是為錯誤處理提供更多的語法支持。那樣的話,可能就不需要重新聲明規則了,沒有它各種相關的復雜性[21]也就不存在了。
? 回到實際的錯誤處理代碼,如果錯誤得到處理,冗長性就會被淡化。良好的錯誤處理通常需要向錯誤添加額外信息。例如,用戶調查中的一個反復出現的評論是關于缺少與錯誤相關的堆棧信息。這可以通過生成并返回增強錯誤的支持函數來解決。在這個例子中,模板代碼的相對數量要小得多:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("invalid integer: %q", b)
}
fmt.Println("result:", x + y)
return nil
}? 新的標準庫功能也可以幫助減少錯誤處理樣板代碼,這與Rob Pike 2015年的博客文章"錯誤就是值"[22]的觀點非常相似。例如在某些情況下,cmp.Or[23]可用于一次處理一系列錯誤:
func printSum(a, b string) error {
x, err1 := strconv.Atoi(a)
y, err2 := strconv.Atoi(b)
if err := cmp.Or(err1, err2); err != nil {
return err
}
fmt.Println("result:", x+y)
return nil
}? 編寫、閱讀和調試代碼都是完全不同的工作。編寫重復的錯誤檢查可能很乏味,但今天的 IDE 提供了強大的、甚至是 LLM 輔助的代碼補全。編寫基本的錯誤檢查對這些工具來說很簡單。在閱讀代碼時冗長性最明顯,但工具在這里也可能有所幫助;例如,有 Go 語言設置的 IDE 可以提供一個切換開關來隱藏錯誤處理代碼。
? 在調試錯誤處理代碼時,能夠快速添加println或有一個專門的行位置來在調試器中設置斷點會很有幫助。當已經有專門的if語句時,這很容易。但如果所有錯誤處理邏輯都隱藏在check、try或?后面,代碼可能必須首先更改為普通的if語句,這會使調試復雜化,甚至可能引入一些錯誤。
? 還有實際的考慮:想出一個新的錯誤處理語法想法很容易;因此社區提出了大量的提案。想出一個經得起審查的好解決方案:就不那么容易了。正確設計語言變更并實際實現它需要協調一致的努力。真正的成本仍然在后面:所有需要更改的代碼、需要更新的文檔、需要調整的工具。綜合考慮,語法變更非常昂貴,Go 團隊相對較小,還有很多其他優先事項要處理。
? 最后一點,我們中的一些人最近有機會參加Google Cloud Next 2025[24],Go團隊在那里有一個展位,我們還舉辦了一個小型的Go聚會。我們有機會詢問的每一位Go用戶都堅決認為我們不應該為了更好的錯誤處理而改變語言。許多人提到,當剛從另一種具有錯誤處理支持的語言轉過來時,Go中缺乏特定的錯誤處理支持最為明顯。隨著人們使用的時間越來越久,這個問題變得不那么重要了。這當然不是一個足夠大的代表性人群,但它是我們在 GitHub上 看到的不同人群。
當然,也有支持變更的理由:
? 缺乏更好的錯誤處理支持仍然是我們用戶調查中最大的抱怨。如果Go團隊真的認真對待用戶反饋,我們最終應該為此做些什么。(盡管似乎也沒有壓倒性的支持[25]語言變更。)
? 也許單一地關注減少字符數不是一個正確的方向。更好的方法可能是使用關鍵字使默認錯誤處理高度可見,同時也要刪除模板代碼(err != nil)。這種方法可能使讀者(代碼審查者)更容易看到錯誤被處理了,而不需要"看多次",從而提高代碼質量和安全性。這將使我們回到check和handle的起點。
? 我們真的不知道現在的冗長問題在多大程度上是錯誤檢查直接導致的。
盡管如此,迄今為止沒有任何解決錯誤處理的嘗試獲得足夠的支持。如果我們誠實地評估我們所處的位置,我們只能承認我們既沒有對問題的共同理解,也不是都同意首先存在問題。考慮到這一點,我們做出以下符合當下的決定:
_在可預見的未來,Go團隊將停止為錯誤處理追求語法語言變更。我們還將關閉所有主要涉及錯誤處理語法的開放和即將提交的提案,不再進一步跟進。
社區在探索、討論和辯論這些問題上投入了巨大的努力。雖然這可能沒有導致錯誤處理語法的任何變化,但這些努力已經為 Go 語言和我們的流程帶來了許多其他改進。也許,在未來的某個時候,關于錯誤處理會出現更清晰的圖景。在那之前,我們期待著將這種令人難以置信的熱情集中在新的機會上,讓Go對每個人都變得更好。
總結一下
1. 問題背景:Go的錯誤處理一直被認為過于冗長,多年來一直是用戶調查中的首先被抱怨的。
2. 歷次嘗試:
? 2018年的 check 和 handle 機制
? 2019年的 try 提案
? 2024年的 ? 操作符提案
3. 最終決定:經過多年嘗試和數百個提案,Go團隊決定在可預見的未來停止追求錯誤處理的語法變更,主要原因包括:
? 沒有達成共識
? 現有方式雖然冗長但足夠好
? 改變會造成社區分裂
? 工具和庫可以幫助緩解問題
4. 未來方向:團隊將關注其他改進Go語言的機會,而不是繼續在錯誤處理語法上投入精力。
由于 Go 長期沒有錯誤處理的解決方案,導致這個問題被拖了很久,從而每個開發者也都有自己的使用習慣,越多人參與討論就越難以達成一致。
引用鏈接
[1] [ On | No ] syntactic support for error handling:https://go.dev/blog/error-syntax
[2]正式提到了這個問題:https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
[3]草案設計:https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md
[4]臭名昭著:https://go.dev/issue/32437#issuecomment-2278932700
[5]try提案:https://go.googlesource.com/proposal/+/master/design/32437-try-builtin.md
[6]tryhard:https://github.com/griesemer/tryhard
[7]GitHub問題:https://go.dev/issue/32437
[8]最近提案:https://research.swtch.com/proposals-large
[11]總體問題:https://go.dev/issue/40432
[12]Go Wiki:https://go.dev/wiki/Go2ErrorHandlingFeedback
[13]2024年上半年Go開發者調查結果:?減少錯誤處理樣板代碼":https://go.dev/issue/71203
[15]Rust:https://www.rust-lang.org/
[16]?操作符:https://doc.rust-lang.org/std/result/index.html#the-question-mark-operator-
[17]討論區:https://go.dev/issue/71460
[18]稍微積極一些:https://github.com/golang/go/discussions/71460#discussioncomment-12060294
[21]復雜性:https://go.dev/blog/errors-are-values
[23]cmp.Or:https://go.dev/pkg/cmp#Or
[24]Google Cloud Next 2025:https://cloud.withgoogle.com/next/25
[25]壓倒性的支持:https://github.com/golang/go/discussions/71460#discussioncomment-11977299

























