你的 HTTP server 收到一個請求,handler 裡有一個 nil pointer dereference:

func handleUser(w http.ResponseWriter, r *http.Request) {
    user := getUser(r.URL.Query().Get("id"))
    fmt.Fprintln(w, user.Name)  // user 是 nil,panic!
}

這個 panic 如果沒有被攔截,會讓整個 server 行程崩潰,所有正在處理的請求都掉了。

但如果你攔截了 panic 然後繼續服務,你可能在一個已經損壞的狀態下繼續執行,製造更難 debug 的問題。

Panic / Crash 的設計,是每個語言對「不可恢復的錯誤應該怎麼辦」的哲學選擇。


Go 的 Panic / Recover

Go 的設計哲學是:panic 表示程式遇到了一個程式設計錯誤(不是業務邏輯錯誤)——nil pointer、index out of bounds、unreachable 的程式碼路徑。

panic 的行為:

  1. 當前函式停止執行
  2. 所有 defer 照正常順序執行
  3. panic 往上冒泡,一層一層往外傳
  4. 如果沒有 recover,整個 goroutine 崩潰

recover:在 defer 函式裡呼叫 recover(),能攔截 panic,讓程式繼續:

func safeHandler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic recovered: %v\n%s", r, debug.Stack())
            http.Error(w, "Internal Server Error", 500)
        }
    }()
 
    handleUser(w, r)  // 如果這裡 panic,被 defer 攔截
}

大多數 web framework(net/httpginecho)都在最外層加了這個 recover middleware——一個 handler 的 panic 不會讓整個 server 崩潰。

什麼時候應該主動 panic

Go 的慣例:只在程式邏輯本身有錯誤的時候,不在業務邏輯錯誤時 panic。

// 可以 panic:這是程式設計錯誤,不應該在生產發生
func mustParseConfig(data []byte) Config {
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        panic(fmt.Sprintf("invalid config: %v", err))  // 啟動時的設定解析,失敗應該 crash
    }
    return cfg
}
 
// 不要 panic:這是業務邏輯錯誤,應該 return error
func getUser(id int) (User, error) {
    user, err := db.Query(...)
    if err != nil {
        return User{}, fmt.Errorf("get user: %w", err)  // 不要 panic
    }
    return user, nil
}

Python 的 Uncaught Exception

Python 沒有 panic 的概念。任何 exception 如果沒有被 try/except 攔截,就往上冒泡,最終如果到了最頂層(__main__)還沒有被處理,行程就 crash。

def main():
    process()  # 如果這裡 raise,沒有 try/except
 
main()
# Python 印 traceback 然後以 exit code 1 退出

Web framework 通常在最外層加 exception handler:

# Flask 的例子
@app.errorhandler(Exception)
def handle_exception(e):
    if isinstance(e, HTTPException):
        return e
    app.logger.error("Unhandled exception", exc_info=e)
    return jsonify(error="Internal server error"), 500

Python 的信號機制:某些錯誤(segfault、SIGKILL)是在 C extension 或 OS 層發生的,Python 無法捕獲,行程直接崩潰。faulthandler 模組可以在這種情況下 dump traceback。


Erlang 的「Let It Crash」

Erlang 的哲學是:不要嘗試在出錯的行程裡修復錯誤,讓它崩潰,讓 supervisor 重啟它。

defmodule Worker do
  def process(data) do
    # 遇到意外輸入?不用 try/catch,直接 crash
    {:ok, result} = risky_operation(data)
    result
  end
end
 
defmodule Supervisor do
  use Supervisor
 
  def start_link do
    Supervisor.start_link(__MODULE__, :ok, strategy: :one_for_one)
  end
 
  def init(:ok) do
    children = [Worker]
    supervise(children, strategy: :one_for_one)
  end
end

Worker 崩了,Supervisor 立刻啟動新的 Worker,繼續服務。這個模式讓你不需要防禦性地在每個地方都加 try/catch——崩了就重啟,快速且乾淨。

前提是:Supervisor tree 設計正確,一個 Worker 的崩潰不會影響整個系統的狀態。


三種態度的對比

語言不可恢復錯誤的處理適用場景
Gopanic + 外層 recoverhandler 層 recover,限制崩潰範圍
PythonUncaught exception + framework handler同上
Erlang/ElixirLet it crash + Supervisor restart錯誤隔離在 actor 層,快速重啟
JavaError vs ExceptionError 不應捕獲)JVM 層的不可恢復錯誤直接 crash JVM

後端服務的實務設計

HTTP server 應該在 handler 層 recover panic:一個請求的 bug 不應該讓整個 server 崩潰,讓其他正常請求跟著陪葬。

但 recover 之後不要假裝什麼都沒發生

  • 記 log(含 stack trace)
  • 回傳 500
  • 考慮讓這個 goroutine/worker 退出(如果 panic 可能表示資料已損壞)

啟動時的錯誤應該 crash:如果你的服務在啟動時發現設定檔解析失敗、資料庫連不上,panicos.Exit(1) 是正確的——讓行程崩潰,讓 K8s 的 restart policy 重啟,而不是在半殘的狀態下繼續服務。

defer 是安全的資源清理工具:不論正常路徑還是 panic 路徑,defer 都會執行。在 Go 裡,defer db.Close()defer mu.Unlock() 是防止資源洩漏的標準做法,即使後面發生 panic 也能正確清理。

下一篇:靜態 vs 動態型別