Go 語言的錯誤處理機制引發爭議
最近,有關Go語言的錯誤處理機制在社區中展開了討論,有人認為冗長重復的錯誤處理格式像是回到了上世紀七十年代,而Go語言的開發者給予了反駁。
Go語言的錯誤處理機制可以從支持函數多返回值說起:
在C語言當中常見的做法是保留一個返回值來表示錯誤(比如,read()返回0),或 者保留返回值來通知狀態,并將傳遞存儲結果的內存地址的指針。這容易產生了不安全的編程實踐,因此在像Go語言這樣有良好管理的語言中是不可行的。認識到 這一問題的影響已超出了函數結果與錯誤通訊的簡單需求的范疇,Go的作者們在語言中內建了函數返回多個值的能力。作為例子,這個函數將返回整數除法的兩個部分:
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
多返回值的出現促進了"comma-ok"的模式。有可能失敗的函數可以返回第二個布爾結果來表示成功。作為替代,也可以返回一個錯誤對象,因此像下面這樣的代碼也就不見怪了:
if result, ok := moreMagic(); ok {
/* Do something with result */
}
除此之外,Go語言還提供了Panic/Recover機制,陳皓在“Go語言簡介”中有比較詳細的描述:
對于不可恢復的錯誤,Go提供了一個內建的panic函數,它將創建一個運行時錯誤并使程序停止(相當暴力)。該函數接收一個任意類型(往往是字符串)作為程序死亡時要打印的東西。當編譯器在函數的結尾處檢查到一個panic時,就會停止進行常規的return語句檢查。
下面的僅僅是一個示例。實際的庫函數應避免panic。如果問題可以容忍,最好是讓事情繼續下去而不是終止整個程序。
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }當panic被調用時,它將立即停止當前函數的執行并開始逐級解開函數堆棧,同時運行所有被defer的函數。如果這種解開達到堆棧的頂端,程序就 死亡了。但是,也可以使用內建的recover函數來重新獲得Go程的控制權并恢復正常的執行。 對recover的調用會通知解開堆棧并返回傳遞到panic的參量。由于僅在解開期間運行的代碼處在被defer的函數之內,recover僅在被延期 的函數內部才是有用的。
你可以簡單地理解為recover就是用來捕捉Painc的,防止程序一下子就掛掉了。
Python和Go語言的實踐者Yuval Greenfield在“Why I’m not leaving Python for Go”的博文中批評了Go語言的錯誤處理機制。他首先引用了Go語言的設計者對錯誤處理機制的看法:
在Go語言中,錯誤處理非常重要。語言的設計和規范鼓勵開發人員顯式地檢查錯誤(與其他語言拋出異常然后catch住是不同的)。這種機制某種程度上使得Go語言的代碼冗長重復,但是幸運的是你可以利用一些技巧來把冗長的代碼最小化。
Yuval表示這點他無法忍受,每一次函數的調用都需要if語句來判斷是否出現錯誤,他引用了一段官方的所謂最小化代碼量的錯誤處理示例:
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
Yuval說,這就是在Go語言中調用函數的正確處理方式,甚至連Println的調用都要這樣做。如果不這么做會怎樣呢?Go語言并沒有堅持要采 用這種冗長的錯誤機制。它也允許忽略這些函數調用錯誤。但是這樣做很危險。在下面的例子中,如果第一個Get函數錯誤,那么程序繼續調用第二個函數!這是 非常恐怖的事情。
func main() {
http.Get("http://www.nuke.gov/seal_presidential_bunker")
http.Get("http://www.nuke.gov/trigger_doomsday_device")
}
理論上,我們要求開發人員決不能忽略返回的錯誤。而實際上,只有在一些關鍵性的錯誤上面處理才是必要的。
關于panic/recover機制,Yuval認為也不夠出色,因為連Go的標準庫都不怎么用這種機制:為什么索引溢出的數組要比錯誤格式的字符 串或者失敗的網絡連接更需要panic呢?Go語言希望能夠完全避免異常,但實際上不能,總有一些異常會在某處發生,讓開發人員在錯誤出現時感到困惑。
針對Yuval的批評,Go的開發者Russ Cox做出了回應:
在Go語言中,規定的方式是,函數返回錯誤信息。如果一個文件并不存在,op.Open函數會返回一個錯誤信息。如果你向你一個中斷了的網絡連接里 寫數據,net.Conn里的Write方法會返回一個錯誤。這種狀況在這種程序中是可以預料到的。這種操作就是容易失敗,你知道程序會如何運行,因為 API的設計者通過內置了一種錯誤情況的結果而讓這一切顯得很清楚。
從另一方面講,有些操作基本上不會出錯,所處的環境根本不可能給你提示錯誤信息,不可能控制錯誤。這才是讓人痛苦的地方。典型的例子;一個程序執行 x[j],j值超出數組邊界,這才痛苦。像這樣預料之外的麻煩在程序中是一個嚴重的bug,一般會弄死程序的運行。不幸的是,由于這種情況的存在,我們很 難寫出健壯的,具有自我防御的服務器——例如,可以應付偶然出現的有bug的HTTP請求處理器時,不影響其他服務的啟動和運行。為解決這個問題,我們引 入了恢復機制,它能讓一個go例程從錯誤中恢復,服務余下設定的調用。然而,代價是,至少會丟失一個調用。這是特意而為之的。引用郵件中的原話:“這種設 計不同于常見的異常控制結構,這是一個認真思考后的決定。我們不希望像java語言里那樣把錯誤和異常混為一談。”
Russ Cox針對“為什么數組越界造成的麻煩會比錯誤的網址或斷掉的網絡引出的問題要大?”這個問題給出了自己的答案:
我們沒有一種內聯并行的方法來報告在執行x[j]期間產生的錯誤,但我們有內聯并行的方法報告由錯誤網址或網絡問題造成的錯誤。
使用Go語言中的錯誤返回模式的規則很簡單:如果你的函數在某種情況下很容易出錯,那它就應該返回錯誤。當我調用其它的程序庫時,如果它是這樣寫的,那我不必擔心那些錯誤的產生,除非有真正異常的狀況,我根本沒有想到需要處理它們。
最后,Russ Cox指出Go語言是為大型軟件設計的:
我們都喜歡程序簡潔清晰,但對于一個由很多程序員一起開發的大型軟件,維護成本的增加很難讓程序簡潔。異常捕捉模式的錯誤處理方式的一個很有吸引力 的特點是,它非常適合小程序。但對于大型程序庫,如果對于一些普通操作,你都需要考慮每行代碼是否會拋出異常、是否有必要捕捉處理,這對于開發效率和程序 員的時間來說都是非常嚴重的拖累。我自己做開發大型Python軟件時感受到了這個問題。Go語言的返回錯誤方式,不可否認,對于調用者不是很方便,但這 樣做會讓程序中可能會出錯的地方顯的很明顯。對于小程序來說,你可能只想打印出錯誤,退出程序。對于一些很精密的程序,根據異常的不同,來源的不同,程序 會做出不同的反應,這很常見,這種情況中,try + catch的方式相對于錯誤返回模式顯得冗長。當然,Python里的一個10行的代碼放到Go語言里很可能會更冗長。畢竟,Go語言主要不是針對10行 規模的程序的。
作者:InfoQ/崔康 熱情的技術探索者,資深軟件工程師,InfoQ編輯,從事企業級Web應用的相關工作,關注性能優化、Web技術、瀏覽器等領域。