這個新 Go 錯誤處理提案,能解決問題不?

煎魚 發表於 2022-07-15
Go

大家好,我是煎魚。

Go 語言的一大特色就是它的錯誤機制,因此基本上所有的錯誤處理提案或討論我都會有所檢視和學習,開拓不同的思考視野和解決方法。

今天分享的是 @Cristo García 所提出的提案《Simple Error Handling for Go 2》,略有修改,和煎魚一起學習和討論吧!

Go 必須仍然是 Go

這一個提案的核心觀點是 Go 必須仍然是 Go,這意味著對於錯誤處理的改造需要滿足如下原則:

  • 增加儘可能少的語法。
  • 儘可能明確方便。

本文中的 “我“ 均指代提案作者 @Cristo García,並非正在學習的煎魚。

原想法

原提案作者 @PeterRk 提出了以下思想:

func getDivisorFromDB(key string) (uint, error) {
    //...
}

func GetDivisor(key string) (uint, error) {
    exit := func(err error) (uint, error) {
        return 1, fmt.Errorf("fail to get divisor with key \"%s\": %v", key, err)
    }

    divisor := check(getDivisorFromDB(key), exit)

    //...
    return divisor, nil
}

使用示例:

divisor := check(getDivisorFromDB(key), exit) 

等同於現有的:

divisor, err := getDivisorFromDB(key)
if err != nil {
    return exit(err)  //return err
}

提案作者認為這是一個正確的方向,我們可以改進它(言外之意:現在的還不夠好)。

問題是什麼

原有的這個想法,有如下兩個問題:

  • 包含不明確的返回語句。
  • 有時抽象是不必要的,並且使程式碼更難閱讀。

新想法

為此新的想法需要解決以上兩個問題,@Cristo García 期望達到更好的效果。通過對語法的簡單修改,我們新增 or 關鍵字。

可以得到以下示例:

divisor, err := getDivisorFromDB(key) or return exit(err)

新增加的 or 關鍵字將會檢測最後返回的值(必須是錯誤型別)是否與 nil 不同。若不同,將執行右邊的函式。

我們也可以省略 return,程式碼將繼續執行。它將像在常規 Go 程式碼中一樣被丟棄,這樣該函式就更可重用。

如下示例:

func GetDivisor(key string) (divisor uint, err error) {
    divisor, err = getDivisorFromDB(key) or return
    return
}

也就是 or return 語句後不跟任何東西,是可以的,會預設拋棄掉。

特殊場景:defer

本節只是為了辯論,但我們可以藉此機會為 defer 新增錯誤檢查,看看能不能做一些什麼,得到新的處理方式。

核心思路:如果我們能不把返回的錯誤儲存在一個變數中,並在 defer 中使之或得到觸發,那麼會非常的有意思。

如下示例 1:

defer f.Close() or return errHdl("", fmt.Errorf("couldn't close file"))

不主動顯式宣告變數,若返回值是錯誤型別且不等於 nil,則自動呼叫 or return 右側的函式並進行處理。

如下示例 2:

defer err := f.Close() or return errHdl("couldn't close file", err)

定義接受錯誤的變數 err 變數,能通過 or return 的語法直接傳參進入函式 errHdl 的入參中被使用。

結果

新增了新的 or return 語法後再與原有的錯誤處理機制進行對比,看看如何。

新的:

func Foo(path string) ([]byte, error) {
    errHdlr := func(reason string, err error) ([]byte, error) {
        return nil, fmt.Errorf("foo %s %w", reason, err)
    }
    
    f, err := os.Open(path) or return errHdlr("couldn't open file", err)
    defer f.Close() or return errHdl("", fmt.Errorf("couldn't close file"))
    result, err := io.ReadAll(f) or return errHdlr("couldn't read from file " + path, err)
    return result, nil
}

舊的:

func Foo(path string) ([]byte, error) {
    f, err := os.Open(path)
      if err != nil {
            return nil, fmt.Errorf("foo %s %w", "couldn't open file", err)
      }
      result, err := io.ReadAll(f)
      if err != nil {
            return nil, fmt.Errorf("foo %s %w", "couldn't read from file " + path, err)
      }
      err = f.Close()
      if err != nil {
            return nil, fmt.Errorf("foo %s %w", "couldn't close the file " + path, err)
      }
      return result, nil
}

這是一個非常簡單的例子,但我們已經可以看到其好處。正在閱讀程式碼的程式設計師甚至可以把注意力放在左邊而忽略錯誤處理。

在使用 gofmt 格式化程式碼後,也比較美觀。

如下示例:

f, err := os.Open(path)      or return errHdlr("couldn't open file", err)
defer f.Close()              or return errHdl("", fmt.Errorf("couldn't close file"))
result, err := io.ReadAll(f) or return errHdlr("couldn't read from file " + path, err)

對的很齊。

總結

在這一個新提案中,作者正在做意見徵集的階段。其主要是推行了 or 關鍵字和變數可傳遞至右側函式等多種思路(前段時間我還分享了個左側函式和表示式的提案)。

該作者的目的是想盡可能的方便,並且不寫以往被大家吐槽的 if err != nil,實現更加的簡潔。

你覺得這個提案怎麼樣呢?歡迎在評論區交流和討論。

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

Go 圖書系列

更多閱讀