在 Golang 中不要簡單的返回 err

相反, 添加相關的調試細節。

有些人喜歡抱怨 Go 需要編寫大量的 " iferr!=nil{returnerr}" 代碼塊。這些人並沒有真正理解 Go 的錯誤處理機制。實際上, 他們抱怨的正是處理 Go 錯誤的完全錯誤方式: returnerr是一種反模式。

讓我通過一些示例代碼來解釋我的意思: 一個用於配置 mTLS[1] 連接的輔助庫。("雙向 TLS" 是向服務器證明客戶端身份的一種方式。)

package mtls
import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "os"
)
type ClientConfig struct {
    CAPath   string
    KeyPath  string
    CertPath string
}
func (c *ClientConfig) BuildTLSConfig() (*tls.Config, error) {
    if *c == (ClientConfig{}) {
        return nil, fmt.Errorf("mtls: cannot build tls.Config from empty ClientConfig")
    }
    ret := &tls.Config{}
    if c.CAPath != "" {
        ca, err := os.ReadFile(c.CAPath)
        if err != nil {
            return nil, err // FIXME: BAD, antipattern
        }
        pool := x509.NewCertPool()
        pool.AppendCertsFromPEM(ca)
        ret.RootCAs = pool
    }
    if c.KeyPath != "" || c.CertPath != "" {
        cert, err := tls.LoadX509KeyPair(c.CertPath, c.KeyPath)
        if err != nil {
            return nil, err // FIXME: BAD, antipattern
        }
        ret.Certificates = []tls.Certificate{cert}
    }
    return ret, nil
}

使用這種糟糕的錯誤處理方式, 如果我們在 ClientConfig.CAPath中傳入一個無效路徑 "bad-cert.pem", 會打印什麼?

ERROR: open bad-cert.pem: no such file or directory

來自一個大型代碼庫, 這將是有一定幫助的, 但幫助不大。需要進行大量調試才能找出到底是在哪裏發生的錯誤。不過, 請注意一件事: 標準庫的 os.ReadFile()函數試圖在這裏幫助我們: 它在錯誤消息中添加了 bad-cert.pem文件的名稱。這絕對是一個在調試時會有幫助的細節。我們是否可以從中獲得啓發? 我們是否可以添加更多有助於調試的細節?

@@ -22,7 +22,7 @@ func (c *ClientConfig) BuildTLSConfig() (*tls.Config, error) {
        if c.CAPath != "" {
                ca, err := os.ReadFile(c.CAPath)
                if err != nil {
-                       return nil, err // FIXME: BAD, antipattern
+                       return nil, fmt.Errorf("mtls: building tls.Config from ClientConfig.CAPath: %w", err)
                }
                pool := x509.NewCertPool()
                pool.AppendCertsFromPEM(ca)
@@ -31,7 +31,7 @@ func (c *ClientConfig) BuildTLSConfig() (*tls.Config, error) {
        if c.KeyPath != "" || c.CertPath != "" {
                cert, err := tls.LoadX509KeyPair(c.CertPath, c.KeyPath)
                if err != nil {
-                       return nil, err // FIXME: BAD, antipattern
+                       return nil, fmt.Errorf("mtls: building tls.Config from ClientConfig.KeyPath & .CertPath: %w", err)
                }
                ret.Certificates = []tls.Certificate{cert}
        }

通過這種改進的錯誤處理代碼, 如果我們在 [2] ?

ERROR: mtls: building tls.Config from ClientConfig.CAPath: open bad-cert.pem: no such file or directory

異常支持者可能會說,"這需要如此多的手工編碼工作, 異常堆棧跟蹤會自動完成這些!" 這在某種程度上是正確的。然而, 如果將手工編碼工作視爲一種投資, Go 的這種方式相比異常有以下幾個優勢:

如果我們需要在這裏以編程方式檢測 "文件未找到" 錯誤, 我們可以使用 errors.Is[3] 來優雅地完成, 這要歸功於上面使用了 %w[4] :

if errors.Is(err, fs.ErrNotExist) {
    fmt.Println("err is File Not Found!")
}

檢測更復雜的錯誤時, 程序化地使用 errors.As[5] 是正確的方法。如果我們想生成這種可檢測的錯誤, 我們將需要開始定義自己的錯誤類型, 而不是僅僅使用 fmt.Errorf

有關 Go 中的錯誤處理的更多信息, 我推薦 "Go wiki 上的" 學習錯誤處理 "頁面"[6] 。

附錄: 經過修改的真實生產代碼, 包含複雜的錯誤處理

下面的代碼是從我以前的一個僱主那裏直接獲取的生產代碼, 其中一些重要部分被刪除, 以便於發佈。

這個片段展示了複雜的錯誤處理, 以及如何添加上下文, 這在調試時會非常有用。

注意:"libzzz" 是原始包名稱的替代品, 該包名稱是正在處理的特定協議的名稱。

func (b *Bus) readAndUnpack() ([]byte, error) {
    n, err := io.ReadAtLeast(b.port, b.buf[:], 2)
    got := b.buf[:n]
    if err != nil {
        return nil, newError("libzzz: cannot read 2-byte preamble [got: % 02X] - error: %w", got, err)
    }
    if got[0] != magic_number {
        return nil, newError("libzzz: bad MAGIC NUMBER in response - message starts with: [% 02X] (expected XX...)", got)
    }
    length := int(got[1])
    if length < 5 {
        return nil, newError("libzzz: response too short: len=%d < 5 [% 02X]", length, got)
    }
    // Now that we know the total length, we can read the remaining bytes of the response
    if n < length {
        n, err = io.ReadAtLeast(b.port, b.buf[n:], length-n)
        if err != nil {
            return nil, newError("libzzz: cannot read remaining bytes of a packet [prefix=% 02X][rest=% 02X] - error: %w",
                got, b.buf[len(got):][:n], err)
        }
        got = b.buf[:len(got)+n]
    }
    crc := crc(got[:length-2])
    if crc != [2]byte{got[length-2], got[length-1]} {
        return nil, newError("libzzz: bad CRC [% 02X], expected [...% 02X]", got, crc[:])
    }
    payload := append([]byte(nil), got[2:length-2]...)
    return payload, nil
}

在 Go 中不要 returnerr。相反, 添加調試所需的缺失細節。

參考鏈接

  1. mTLS: https://en.wikipedia.org/wiki/mTLS#mTLS
  2. 如果我們在: https://go.dev/play/p/bw-Q2jFY1U8
  3. errors.Is: https://pkg.go.dev/errors#Is
  4. %w: https://pkg.go.dev/fmt#Errorf
  5. errors.As: https://pkg.go.dev/errors#As
  6. "Go wiki 上的" 學習錯誤處理 "頁面": https://go.dev/wiki/LearnErrorHandling
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/poh_zyY41GRSz9IrOAoq7g