爲什麼 Go 語言的錯誤處理其實設計得很好
Go 的臭名昭著的錯誤處理 [1] 引起了編程語言圈外人士的廣泛關注,常常被認爲是該語言最具爭議的設計決策之一。如果你瀏覽 Github 上任何一個用 Go 編寫的項目,幾乎可以保證你會看到以下代碼行比代碼庫中的其他部分出現得更頻繁:
if err != nil {
return err
}
對於剛接觸這門語言的人來說,這可能顯得多餘且不必要,但 Go 將錯誤視爲一等公民(值)的原因,深深植根於編程語言理論的歷史以及 Go 語言本身的主要目標。人們曾多次嘗試改變或改進 Go 處理錯誤的方式,但到目前爲止,有一個提議勝過了所有其他:
別動
if err != nil![2]
Go 的錯誤哲學
Go 在錯誤處理上的哲學迫使開發者在編寫的大多數函數中將錯誤作爲一等公民來對待。即使你使用類似下面的方式忽略錯誤:
func getUserFromDB() (*User, error) { ... }
func main() {
user, _ := getUserFromDB()
}
大多數靜態檢查工具或 IDE 都會發現你在忽略錯誤,而且在代碼審查時,這一點對你的團隊成員來說也顯而易見。然而,在其他語言中,你的代碼可能並未明確處理 try catch 代碼塊中的潛在異常,控制流的處理完全不透明。
如果你按照 Go 的標準方式處理錯誤,你將獲得以下好處:
-
- 沒有隱藏的控制流
-
- 不會有意外的
未捕獲異常日誌炸燬你的終端(除了通過 panic 導致的實際程序崩潰)
- 不會有意外的
-
- 對代碼中的錯誤擁有完全控制權,作爲你可以處理、返回或隨意操作的 值
不僅 func f() (value, error) 的語法對新手來說易於教授,而且在任何 Go 項目中都是一種標準,確保了一致性。
需要注意的是,Go 的錯誤語法並不會強制你處理程序可能拋出的每一個錯誤。Go 只是提供了一種模式,讓你將錯誤視爲程序流程中的關鍵部分,但僅此而已。在程序結束時,如果發生了錯誤,你通過 err != nil 發現了它,而你的應用程序沒有采取任何實際行動,無論如何你都會陷入麻煩——Go 救不了你。讓我們看一個例子:
if err := criticalDatabaseOperation(); err != nil {
// 僅僅記錄錯誤而不返回它來停止控制流(很糟糕!)
log.Printf("數據庫出錯了:%v", err)
// 我們應該在這行下面加上 `return`!
}
if err := saveUser(user); err != nil {
return fmt.Errorf("無法保存用戶:%w", err)
}
如果調用 criticalDatabaseOperation() 時出錯,err != nil,我們除了記錄日誌外沒有對錯誤做任何處理!我們可能面臨數據損壞或其他未被智能處理的意外問題,比如通過重試函數調用、取消後續程序流程,或者在最壞的情況下關閉程序。Go 並不神奇,無法在這些情況下救你。Go 只是提供了一種返回和使用錯誤作爲值的標準方法,但你仍然需要自己想辦法處理這些錯誤。
其他語言如何處理:拋出異常
在類似 JavaScript Node.js 運行時中,你可以按如下方式組織程序,這種方式稱爲拋出 異常:
try {
criticalOperation1();
criticalOperation2();
criticalOperation3();
} catch (e) {
console.error(e);
}
如果這些函數中任何一個發生錯誤,運行時會彈出錯誤的堆棧跟蹤並記錄到控制檯,但並沒有明確的程序化處理來說明哪裏出了問題。
你的 criticalOperation 函數不需要顯式處理錯誤流,因爲 try 塊中發生的任何異常都會在運行時連同出錯的堆棧跟蹤一起拋出。基於異常的語言的一個好處是,與 Go 相比,即使是未處理的異常也會在運行時通過堆棧跟蹤拋出。而在 Go 中,完全不處理關鍵錯誤是可能的,這可以說要糟糕得多。Go 提供了錯誤處理的完全控制權,但也帶來了完全的責任。
注: 異常當然不是其他語言處理錯誤的唯一方式。例如,Rust 通過使用選項類型和模式匹配來發現錯誤條件,利用一些漂亮的語法糖實現了類似的結果,這是一個很好的折衷方案。
爲什麼 Go 不使用異常來處理錯誤
Go 的禪意
Go 的禪意提到了兩條重要格言:
-
- 簡單至上
-
- 爲失敗做計劃,而不是成功
在所有返回 (value, error) 的函數中使用簡單的 if err != nil 片段,有助於確保程序中的失敗被放在 首要位置 考慮。你不需要糾纏於複雜嵌套的 try catch 塊來適當地處理所有可能拋出的異常。
基於異常的代碼往往不透明
然而,在基於異常的代碼中,你必須意識到代碼可能出現異常的每一種情況,而實際上並未處理它們,因爲它們會被 try catch 塊捕獲。也就是說,它鼓勵程序員不去檢查錯誤,因爲他們知道至少在運行時某些異常會自動被處理。
在基於異常的編程語言中,一個函數可能經常看起來像這樣:
item = getFromDB()
item.Value = 400
saveToDB(item)
item.Text = 'price changed'
這段代碼沒有采取任何措施來確保異常被正確處理。或許讓上述代碼意識到異常的區別在於將 saveToDB(item) 和 item.Text = 'price changed 的順序調換,這種方式是不透明的,難以推理,並且可能助長一些懶散的編程習慣。在函數式編程術語中,這被稱爲高大上的術語:違反引用透明性 [3]。微軟工程博客在 2005 年的一篇博文 [4] 至今仍然適用,文中寫道:
我的觀點不是說異常不好。我的觀點是異常太難了,我不夠聰明去處理它們。
Go 錯誤語法的優點
輕鬆創建可操作的錯誤鏈
if err != nil 模式的一個超能力在於,它允許輕鬆構建錯誤鏈,貫穿程序的層級直到需要處理它們的地方。例如,一個常見的 Go 錯誤可能在程序的 main 函數中被處理,讀取如下:
[2020-07-05-9:00] 錯誤:無法創建用戶:無法檢查用戶是否已存在於數據庫中:無法建立數據庫連接:無網絡
上述錯誤 (a) 清晰,(b) 可操作,(c) 提供了應用程序出錯層的足夠上下文。與其爆出一堆難以理解的加密堆棧跟蹤,不如像上面展示的那樣,通過我們可以添加人類可讀上下文的因素來創建錯誤,並通過清晰的錯誤鏈進行處理。
此外,這種錯誤鏈作爲標準 Go 程序結構的一部分自然產生,可能看起來像這樣:
// 在 controllers/user.go 中
if err := db.CreateUser(user); err != nil {
return fmt.Errorf("無法創建用戶:%w", err)
}
// 在 database/user.go 中
func (db *Database) CreateUser(user *User) error {
ok, err := db.DoesUserExist(user)
if err != nil {
return fmt.Errorf("無法檢查用戶是否已存在於數據庫中:%w", err)
}
...
}
func (db *Database) DoesUserExist(user *User) error {
if err := db.Connected(); err != nil {
return fmt.Errorf("無法建立數據庫連接:%w", err)
}
...
}
func (db *Database) Connected() error {
if !hasInternetConnection() {
return errors.New("無網絡連接")
}
...
}
上面代碼的美妙之處在於,每個錯誤都被各自的函數完全命名空間化,具有信息量,並且只負責它們所知道的部分。使用 fmt.Errorf("出錯了:%w", err) 這種錯誤鏈方式,使得構建出色的錯誤消息變得輕而易舉,這些消息可以根據 你 的定義準確告訴你哪裏出了問題。
在此基礎上,如果你還想爲函數附加堆棧跟蹤,可以使用出色的 github.com/pkg/errors[5] 庫,它提供瞭如下函數:
errors.Wrapf(err, "無法保存帶有郵箱 %s 的用戶", email)
這些函數會打印出堆棧跟蹤 連同 你通過代碼創建的人類可讀錯誤鏈。如果我能總結出我收到的關於編寫地道 Go 錯誤處理的最重要建議:
-
1. 當你的錯誤對開發者有操作意義時,添加堆棧跟蹤
-
2. 對返回的錯誤採取行動,不要只是把它們冒泡到 main,記錄下來,然後忘掉
-
3. 保持你的錯誤鏈清晰無歧義
當我編寫 Go 代碼時,錯誤處理是我 從不 擔心的一件事,因爲錯誤本身是我編寫的每個函數的核心部分,賦予我以可讀且負責任的方式安全處理它們的完全控制權。
“if ...; err != nil” 是你寫 Go 時可能會敲下的代碼。我不認爲這是優點或缺點。它能完成任務,易於理解,並且賦予程序員在程序失敗時做出正確選擇的權力。其餘的取決於你。
——來自 Hacker News[6]
引用鏈接
[1] 臭名昭著的錯誤處理:https://github.com/golang/go/issues/32825
[2]別動 if err != nil!:https://github.com/golang/go/issues/32825
[3]違反引用透明性:https://stackoverflow.com/questions/28992625/exceptions-and-referential-transparency/28993780#28993780
[4]博文:https://devblogs.microsoft.com/oldnewthing/?p=36693
[5]github.com/pkg/errors:https://godoc.org/github.com/pkg/errors
[6]Hacker News:https://news.ycombinator.com/item?id=20303468
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/J6Jipkf27Go-_vmmXUiZWg