在 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 的這種方式相比異常有以下幾個優勢:
-
通過編寫自定義消息, 我們可以提供更多有用的調試細節 (例如,
os.ReadFile爲我們提供了文件名); -
由於不依賴於代碼行號, 這些消息的生命週期更長, 並且實際上可以在沒有源代碼的情況下被理解和推理。
如果我們需要在這裏以編程方式檢測 "文件未找到" 錯誤, 我們可以使用 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。相反, 添加調試所需的缺失細節。
參考鏈接
- mTLS: https://en.wikipedia.org/wiki/mTLS#mTLS
- 如果我們在: https://go.dev/play/p/bw-Q2jFY1U8
errors.Is: https://pkg.go.dev/errors#Is%w: https://pkg.go.dev/fmt#Errorferrors.As: https://pkg.go.dev/errors#As- "Go wiki 上的" 學習錯誤處理 "頁面": https://go.dev/wiki/LearnErrorHandling
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/poh_zyY41GRSz9IrOAoq7g