Rust 錯誤處理 – panic! 宏
簡介
在 Rust 中,錯誤處理分為 可恢復錯誤(Result)與 不可恢復錯誤(panic!)。panic! 代表程式在執行時遇到了無法安全繼續的情況,會立即終止當前執行緒,並向上層傳遞一個錯誤訊息。雖然在大多數情況下我們希望透過 Result 來處理錯誤,但在以下情境中,panic! 仍是最直接、最符合語意的選擇:
- 程式的前提條件(precondition)被破壞,例如傳入了不合法的參數。
- 內部不變式(invariant)被違反,表示程式本身已經進入了未定義狀態。
- 測試或開發階段需要快速定位錯誤來源。
了解 panic! 的運作機制、如何自訂錯誤訊息以及在何時該使用它,對於寫出 安全且可維護 的 Rust 程式至關重要。
核心概念
1. panic! 的基本語法
panic! 是一個 宏,接受類似 format! 的參數,會在執行時拋出一個 panic。最簡單的寫法:
panic!("發生致命錯誤");
如果想要插入變數:
let value = 42;
panic!("不允許的值:{}", value);
2. Panic 與 unwind / abort
編譯時可以選擇兩種 panic 行為:
| 行為 | 說明 |
|---|---|
unwind(預設) |
產生堆疊回溯(stack unwind),允許 catch_unwind 捕獲 panic,適合測試或需要清理資源的情況。 |
abort |
直接終止程式,不產生回溯,執行檔大小較小,適合嵌入式或對效能極度敏感的應用。 |
在 Cargo.toml 中設定:
[profile.release]
panic = "abort"
3. 捕獲 panic:std::panic::catch_unwind
雖然大多數情況下不建議捕獲 panic,但在 測試框架、插件系統 或 多執行緒容錯 時會用到:
use std::panic;
let result = panic::catch_unwind(|| {
// 可能會 panic 的程式碼
panic!("故意觸發");
});
match result {
Ok(_) => println!("沒有 panic"),
Err(err) => println!("捕獲到 panic: {:?}", err),
}
⚠️
catch_unwind只能捕獲unwind模式的 panic;若編譯為abort,此函式會直接導致程式終止。
4. 自訂 panic 鉤子:set_hook
Rust 允許在程式啟動時註冊一個全域的 panic 鉤子,用來改寫錯誤訊息的輸出方式(例如寫入日誌檔):
use std::panic;
fn main() {
panic::set_hook(Box::new(|info| {
// 只顯示簡短訊息,避免泄漏敏感資訊
eprintln!("程式發生致命錯誤: {}", info);
}));
// 觸發 panic,會走自訂的 hook
panic!("測試自訂 hook");
}
5. Panic 與多執行緒
在多執行緒環境中,每個執行緒的 panic 只會終止該執行緒,不會直接影響其他執行緒或主執行緒(除非使用 join 時傳回錯誤):
use std::thread;
let handle = thread::spawn(|| {
panic!("子執行緒失敗");
});
match handle.join() {
Ok(_) => println!("子執行緒正常結束"),
Err(err) => println!("子執行緒 panic: {:?}", err),
}
程式碼範例
以下提供 5 個實用範例,展示 panic! 在不同情境下的使用方式。
範例 1:檢查函式前提條件
fn divide(dividend: i32, divisor: i32) -> i32 {
// 若除數為 0,直接 panic,因為此情況無法安全回傳結果
if divisor == 0 {
panic!("除數不能為 0");
}
dividend / divisor
}
fn main() {
println!("{}", divide(10, 2)); // 5
// println!("{}", divide(10, 0)); // 會 panic
}
重點:使用
panic!來表達「此函式在此情況下不應該被呼叫」。
範例 2:使用 catch_unwind 捕獲 panic(測試用)
use std::panic;
fn may_panic(flag: bool) {
if flag {
panic!("故意觸發 panic");
}
}
fn main() {
let ok = panic::catch_unwind(|| may_panic(false));
assert!(ok.is_ok());
let err = panic::catch_unwind(|| may_panic(true));
assert!(err.is_err());
}
範例 3:自訂 panic 鉤子寫入日誌
use std::panic;
use std::fs::OpenOptions;
use std::io::Write;
fn init_panic_logger() {
panic::set_hook(Box::new(|info| {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("panic.log")
.unwrap();
let _ = writeln!(file, "Panic 發生: {}", info);
}));
}
fn main() {
init_panic_logger();
panic!("這是一個測試 panic,會寫入 panic.log");
}
範例 4:在多執行緒中處理 panic
use std::thread;
fn main() {
let handles: Vec<_> = (0..3).map(|i| {
thread::spawn(move || {
if i == 1 {
panic!("執行緒 {} 發生 panic", i);
}
println!("執行緒 {} 正常結束", i);
})
}).collect();
for h in handles {
match h.join() {
Ok(_) => println!("子執行緒完成"),
Err(e) => eprintln!("捕獲到子執行緒 panic: {:?}", e),
}
}
}
範例 5:在 abort 模式下減少二進位大小
# Cargo.toml
[profile.release]
panic = "abort"
fn main() {
// 在 release 版會直接 abort,沒有 unwind 的成本
panic!("此程式在 release 時會直接 abort");
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的做法 |
|---|---|---|
把所有錯誤都寫成 panic! |
會讓程式在可恢復的錯誤情況下直接崩潰,使用者體驗差。 | 只在 不可能恢復 或 程式邏輯錯誤 時使用 panic!,其他情況使用 Result、Option。 |
在庫(crate)中直接 panic! |
使用者無法自行決定錯誤處理方式,可能破壞上層應用。 | 提供 Result API,或在 panic! 前加上 #[must_use] 的警告。 |
忘記在 Cargo.toml 設定 panic = "abort" |
在嵌入式或性能敏感的環境仍使用 unwind,導致不必要的程式碼膨脹。 |
依需求明確設定 panic 行為,並在 CI 中測試兩種模式。 |
在 catch_unwind 中使用 ? |
? 只能傳播 Result,無法捕獲 panic!,會導致編譯錯誤。 |
若需要同時處理 Result 與 panic!,分別使用 match 或 Result::map_err 包裝。 |
在 set_hook 中執行可能 panic 的程式 |
若 hook 本身 panic,會導致 雙重 panic,最終 abort。 | Hook 內部保持簡單、避免使用可能失敗的 I/O,或使用 std::panic::resume_unwind 重新拋出。 |
最佳實踐:
- 明確文件化:在函式或模組的文件註解中說明何時會 panic。
- 使用
debug_assert!:在開發階段檢查不變式,發佈版會自動移除,避免不必要的 panic。debug_assert!(index < vec.len(), "索引超出範圍"); - 最小化 panic 範圍:將可能 panic 的程式碼封裝在獨立函式,方便測試與未來改寫。
- 在測試中使用
#[should_panic]:驗證程式在不合法輸入時確實會 panic。#[test] #[should_panic(expected = "除數不能為 0")] fn test_divide_by_zero() { divide(10, 0); }
實際應用場景
| 場景 | 為什麼選擇 panic! |
|---|---|
| CLI 工具的參數驗證 | 若使用者提供了錯誤的參數,直接 panic 可以快速終止並顯示錯誤訊息,避免後續不必要的運算。 |
| 嵌入式系統的致命錯誤 | 設備在硬體資源錯誤(例如記憶體損毀)時,最安全的做法是立即 abort,防止不確定行為。 |
| 測試框架 | Rust 的測試框架本身就是以 panic 為基礎,#[should_panic] 能驗證程式在特定條件下會崩潰。 |
| 插件/腳本執行環境 | 主程式需要保護自己不被惡意或錯誤的插件拖垮,使用 catch_unwind 包住插件執行,插件內部若 panic,主程式仍能繼續運行。 |
| 資料庫交易的不可恢復錯誤 | 若交易過程中發現資料不一致,最安全的方式是 abort 當前交易(panic),讓外層回滾機制接管。 |
總結
panic! 在 Rust 中扮演 不可恢復錯誤 的角色,提供了一條清晰且語意明確的路徑,讓程式在遇到「此時此刻已無法安全繼續」的情況下立刻停止。掌握以下要點,才能在實務開發中正確運用:
- 僅在真正不可恢復的情況下使用,其餘錯誤應以
Result/Option處理。 - 了解
unwindvsabort的差異,根據目標平台與效能需求選擇編譯設定。 - 透過
catch_unwind、set_hook以及 多執行緒的join,在需要的時候安全捕獲或記錄 panic。 - 文件化 與 測試 是避免意外 panic 的最佳防線。
熟練 panic! 的使用方式,能讓你的 Rust 程式在面對致命錯誤時保持可預測、易除錯的特性,同時不犧牲程式的安全與效能。祝你在 Rust 的錯誤處理旅程中,寫出更穩定、更可靠的程式碼!