在 Go 中應該何時使用 panic?

如果你使用 Go 有一段時間了,可能聽說過 Go 的諺語 "don't panic"。

這句話的核心意思是:“優雅地處理錯誤,或者將錯誤返回給調用方去處理,而不是直接傳遞給內置的 panic() 函數。”

儘管 “不要 panic” 是一個值得遵循的優秀準則,但有時候它被理解成“你絕對不能、絕不應該調用 panic()”。我並不這麼認爲,在某些少數情況下, panic() 是更合適的行爲。

「爲什麼 panic 被視爲 “不好”?」

panic() 本身並不是不好。事實上它的行爲(執行 defer、打印棧)還是挺有用的。

真正的問題在於:返回錯誤通常更好。

當你在函數中調用 panic() 時,它觸發的是一個固定流程;而如果你返回錯誤,則由調用方決定如何處理這個錯誤。他們可以記錄日誌、提示用戶、重試操作,甚至忽略它。靈活性更強。

返回錯誤還有其他優勢:

綜上,**返回錯誤或就地處理幾乎總是更優選擇。**但 panic() 依然有用,只是需要你非常謹慎地使用。

「什麼時候適合 panic ?」

爲了回答這個問題,我們可以將錯誤分爲兩類:

操作錯誤:

我們指的是在程序運行期間可能合理預期會發生的錯誤。一些例子如文件權限錯誤、長時間運行的操作超時或無效用戶輸入引起的錯誤。這些錯誤並不一定意味着你的程序本身有問題——事實上,它們通常是由程序無法控制的外部因素引起的。

操作錯誤是可以預期的。因爲你知道它們在正常操作期間有可能發生,所以你應該努力將它們返回給調用者。不要使用 panic() 來管理它們。

程序員錯誤:

我們指的是在程序運行期間 "永遠" 不應該發生的錯誤——這種錯誤源於開發人員的錯誤、代碼庫中的邏輯缺陷,或試圖以不支持的方式使用語言特性或函數。理想情況下,你會在開發或測試期間發現程序員錯誤,而不是讓它們在生產中暴露出來。

當你遇到程序員錯誤時,意味着你的程序處於意外狀態。在這種情況下,調用 panic() 更普遍被認爲是合適的行爲。

除上述討論之外,在以下情況下,使用 panic() 可能是一個好的適當的選擇:

你可以在觸發 panic 的一些 Go 標準庫操作中看到這種邏輯。例如:

這些有什麼共同點?

首先,它們都是程序員錯誤。如果發生這些情況,是由於代碼庫中的邏輯錯誤,或者你試圖以不支持的方式使用語言特性或函數。這些情況不應該在正常的生產操作中發生。

而且如果它們返回錯誤,會給每個人的 Go 代碼增加可以說是不可接受的額外錯誤處理。想象一下,如果你每次使用 / 運算符、訪問切片中的值或解鎖互斥鎖時都必須檢查錯誤返回值,這會增加很多開銷。

實際示例

到目前爲止,我希望清楚 panic() 應該謹慎被使用,並且只在真正有意義的時候使用。就我個人而言,我工作的大約一半代碼庫根本不調用 panic(),即使調用,也只是在少數地方。

以下是幾個來自我最近工作代碼庫的實際示例:

示例一

這是一個來自 Web 應用程序的示例,其中有一些代碼從 HTTP 請求上下文中檢索用戶值。

type contextKey string

const userContextKey = contextKey("user")

func contextGetUser(r *http.Request) user.User {
 user, ok := r.Context().Value(userContextKey).(user.User)
 if !ok {
  panic("missing user value in request context")
 }

 return user
}

在這個特定的應用程序中,代碼的結構使得 contextGetUser() 函數只在邏輯上期望請求上下文中存在用戶值時被調用。在這個應用程序中,缺少值絕對是一個程序員錯誤,表明代碼庫存在問題。

當然,contextGetUser() 可以返回錯誤而不是 panic。但這個函數被調用很多次,感覺返回錯誤會爲我們在正常操作中永遠不應該看到的東西引入過多的錯誤處理。權衡之下,在這裏使用 panic() 感覺是合適的。

示例二

這是來自同一應用程序的另一個示例:

func getEnvInt(key string, defaultValue int) int {
 value, exists := os.LookupEnv(key)
 if !exists {
  return defaultValue
 }

 intValue, err := strconv.Atoi(value)
 if err != nil {
  panic(err)
 }

 return intValue
}

在這個應用程序中,getEnvInt() 是一個輔助函數,用於從環境變量讀取值並將其轉換爲 int。如果轉換失敗,則 panic。

乍一看,這似乎不是使用 panic 的合適地方。嘗試將特定環境變量轉換爲 int 時的錯誤似乎是我們程序控制之外的東西——一個操作錯誤。確實如此。

但在這種情況下,getEnvInt 函數在程序開始時用於從環境加載配置設置,如下所示:

httpPort := getEnvInt("HTTP_PORT", 3939)

在程序的這個早期階段,日誌記錄器(它也依賴於環境設置)尚未初始化。由於程序無法在沒有有效配置值的情況下運行,並且還沒有適當的日誌記錄器來優雅地處理錯誤,因此沒有其他好的選項來管理這個錯誤。訴諸 panic() 感覺是一個合理的選擇。

這符合 "你不希望程序繼續運行,並且沒有更好的選項來處理錯誤" 的場景。

注意:我可以讓 getEnvInt() 函數返回錯誤,並讓調用者自己調用 panic()。但這會爲基本相同的結果生成額外的錯誤處理,因此權衡之下,從 getEnvInt() 內部 panic 是有意義的。

示例三

這是一個我之前在防護條款中使用 panic 的示例。

var safeChars = regexp.MustCompile("^[a-z0-9_]+$")

type SortValues struct {
 Column    string
 Ascending bool
}

func (sv *SortValues) OrderBySQL() string {
 if !safeChars.MatchString(sv.Column) {
  panic("unsafe sort column: " + sv.Column)
 }

 if sv.Ascending {
  return fmt.Sprintf("ORDER BY %s ASC", sv.Column)
 }

 return fmt.Sprintf("ORDER BY %s DESC", sv.Column)
}

在這個特定的應用程序中,需要根據用戶輸入生成帶有動態 ORDER BY 參數的 SQL 查詢。

SortValues 類型保存用戶提供的列名和排序方向,其 OrderBySQL() 方法返回一個像 ORDER BY title ASC 這樣的字符串。

在調用 OrderBySQL() 方法時,上游函數應該已經根據允許列名的白名單驗證了 SortValues.Column 值。但如果由於錯誤或疏忽導致該驗證步驟被遺漏,應用程序將容易受到通過用戶提供的列名的 SQL 注入攻擊。

因此,作爲最後的緩解措施,我們在 OrderBySQL() 中使用 panic 防護條款來確保 SortValues.Column 值只包含 "安全" 字符(a 到 z,0 到 9 和下劃線)。

從 OrderBySQL() 返回錯誤似乎有些過頭。但如果它確實發生了,觸發 panic 比冒着危害數據庫的風險感覺更好。

本文翻譯自《When is it OK to panic in Go?》,並在此基礎上進行了總結與概括。如需瞭解更多細節,歡迎查閱原文:https://www.alexedwards.net/blog/when-is-it-ok-to-panic-in-go


References
https://www.alexedwards.net/blog/when-is-it-ok-to-panic-in-go

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/D9GdelAi4pTLqQCb97aWLA