Go 的錯誤處理#
創建日期:2021 年 12 月 7 日 下午 6:24
標籤:Golang
發佈日期:2021 年 12 月 29 日
問題#
Go 語言中一個令人困擾的問題就是錯誤處理。
每個函數都需要返回一個錯誤,每次返回都需要進行一次判斷,導致核心調用代碼的邏輯部分超過一半的行數都用於處理錯誤,而且每次處理的代碼都完全相同,既冗余又無意義。對於之前接受了 Python 簡潔哲學熏陶的我來說,這是一種極大的痛苦。
好在這個問題是可以優化的。當然,更好的可讀性肯定會牺牲一定的性能,但是考慮到 Go 本身的性能已經非常出色,而其可讀性已經是一個尾大不掉的問題了。在處理邏輯較為複雜的地方,使用這種方式來減少 Go 的錯誤處理代碼,可以說是利大於弊的。
解決方案#
下面我們來示範一下,
// 我們先來一段代碼
// 處理string判斷的被調用函數
func A(a, b string) (string, error) {
if a != b {
return "", errors.NewErr("info")
}
return a, nil
}
// 處理int判斷的被調用函數
func B(a, b int) (int, error) {
if a != b {
return 0, errors.NewErr("info")
}
return a, nil
}
// 主函數
func Main(a, b string, c, d int) error {
// 普通寫法下,每次調用都需要處理一次錯誤
res, err := A(a, b)
if err != nil {
return err
}
res, err = B(c, d)
if err != nil {
return err
}
// 此處略去100次調用
return nil
}
從上面這段代碼,可以看出,每次主函數調用一次其他函數,就需要處理一次返回,一行調用,三行錯誤處理,調用的邏輯完全淹沒在了錯誤處理中。
我們的解決辦法就是修改一下被調用的函數,讓它們也學會處理一下錯誤,將主函數的錯誤處理移動到每個函數中,這樣,這個函數複用的次數越多,節省的代碼就越多。而且可讀性會有很好的提升。
下面我來示範一下
func A(a, b string, err error) (string, error) {
// 增加了一個字段err,和以下邏輯
// 接收一個error參數,如果非空就返回
if err != nil {
return "", err
}
if a != b {
return "", errors.NewErr("info")
}
return a, nil
}
func B(a, b int) (int, error) {
if err != nil {
return 0, nil
}
if a != b {
return 0, errors.NewErr("info")
}
return a, nil
}
func Main(a, b string, c, d int) error {
res, err := A(a, b, nil)
res, err = B(c, d, err)
// 此處略去100次調用
// 可以看出這裡無論調用多少次,只用寫一次錯誤處理
if err != nil {
return err
}
return nil
}
反思#
那麼這種方式是否可以應用在所有的地方來解決我們的錯誤處理問題呢?
我覺得這並不是萬能的處理方法,它至少有以下幾個問題:
- 雖然一旦其中一個函數出現了錯誤,後續的函數其實不會真的執行,但是還是存在函數調用和參數傳遞,所以一定是有一定的性能損耗的,如果我們追求極致性能的部分,不適宜使用這種方式。
- 這種方式會增加一些 debug 定位的難度,因為是執行完所有的邏輯後才返回錯誤信息,所以不能以非常直觀的方式來展示具體錯誤的位置。當然,也是有解決辦法的,我們可以通過給錯誤消息中加入函數堆棧信息,但是就稍微麻煩了一些。
總結#
這種錯誤處理方式可以很好地提升我們代碼的可讀性,但是會對性能造成細微的影響,並且會增加一點 debug 的難度。但是對於較為複雜的代碼,這點損失換來的高可讀性,我個人認為是值得的。