你的 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 的行為:
- 當前函式停止執行
- 所有
defer照正常順序執行 - panic 往上冒泡,一層一層往外傳
- 如果沒有
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/http、gin、echo)都在最外層加了這個 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"), 500Python 的信號機制:某些錯誤(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
endWorker 崩了,Supervisor 立刻啟動新的 Worker,繼續服務。這個模式讓你不需要防禦性地在每個地方都加 try/catch——崩了就重啟,快速且乾淨。
前提是:Supervisor tree 設計正確,一個 Worker 的崩潰不會影響整個系統的狀態。
三種態度的對比
| 語言 | 不可恢復錯誤的處理 | 適用場景 |
|---|---|---|
| Go | panic + 外層 recover | handler 層 recover,限制崩潰範圍 |
| Python | Uncaught exception + framework handler | 同上 |
| Erlang/Elixir | Let it crash + Supervisor restart | 錯誤隔離在 actor 層,快速重啟 |
| Java | Error vs Exception(Error 不應捕獲) | JVM 層的不可恢復錯誤直接 crash JVM |
後端服務的實務設計
HTTP server 應該在 handler 層 recover panic:一個請求的 bug 不應該讓整個 server 崩潰,讓其他正常請求跟著陪葬。
但 recover 之後不要假裝什麼都沒發生:
- 記 log(含 stack trace)
- 回傳 500
- 考慮讓這個 goroutine/worker 退出(如果 panic 可能表示資料已損壞)
啟動時的錯誤應該 crash:如果你的服務在啟動時發現設定檔解析失敗、資料庫連不上,panic 或 os.Exit(1) 是正確的——讓行程崩潰,讓 K8s 的 restart policy 重啟,而不是在半殘的狀態下繼續服務。
defer 是安全的資源清理工具:不論正常路徑還是 panic 路徑,defer 都會執行。在 Go 裡,defer db.Close()、defer mu.Unlock() 是防止資源洩漏的標準做法,即使後面發生 panic 也能正確清理。