作者註: 這是一篇在 2019 年開了頭卻未能完成的草稿,於 2026 年藉由 AI 協作補齊內容,並保留了 2019 年當時的技術生態與時空視角。
在上一篇 Learning Rust 中,我分享了自己接觸 Rust 的背景、為了看懂 Fuchsia OS 系統服務而重拾這門語言的轉折點,以及幾本適合入門打底的書籍。
上一篇主要是全面性的入門材料。在跨過基礎語法、所有權(ownership)與借用(borrowing)的第一道門檻後,進入中進階階段時,往往會面臨更多細節設計的挑戰。這篇接續先前的承諾,整理我覺得極具啟發性的進階資源、精選文章、初學者容易卡關的語義盲點(例如 Option::take() 的不可或缺),以及身為長期 Go 語言愛好者,在 2019 年如何看待 Go 與 Rust 的定位抉擇。
進階書籍:深入 Unsafe 的底層規則
當你寫了一陣子 safe Rust,想要進一步探究效能極致、自訂底層資料結構,或是與 C 語言進行 FFI(Foreign Function Interface)介接時,就必須推開 unsafe 的大門:
- The Rustonomicon(俗稱死靈之書 / 諾米康)
很多人剛學 Rust 時會有一種誤解,以為寫 Rust 就應該完全杜絕 unsafe。但在現實世界中,如果沒有 unsafe,像 Vec、LinkedList 這些標準函式庫的核心容器便無法實現。
《The Rustonomicon》就是官方專門為這群想要了解底層機制的開發者撰寫的進階指南。它深入探討了記憶體佈局(memory layout)、未定義行為(Undefined Behavior, UB)、aliasing 規則、指標轉型以及 panic 安全(panic safety)與 RAII 銷毀保證。
即使你在日常開發中只寫 safe Rust,我也非常推薦抽空讀讀這本書。當你理解了編譯器背後在防範什麼底層未定義行為時,你就能從機制層面理解其設計哲學,不再把生命週期(lifetimes)與 borrow checker 的嚴格檢查視為單純的束縛。
精選技術文章
網路上有很多優秀的工程師針對 Rust 的特定主題寫了深入淺出的文章,以下幾篇是我在學習過程中覺得含金量極高的技術分享:
閉包的本質:Finding Closure in Rust
Rust 的閉包(closures)依捕獲環境的方式與呼叫約束,實作了 FnOnce、FnMut 與 Fn 三種 trait(其繼承層級為 FnOnce ⊃ FnMut ⊃ Fn)。初學者常常會被這三個 trait 搞得暈頭轉向。
Huon Wilson 這篇文章以非常清晰的步驟,剖析編譯器如何將閉包「解糖(desugar)」為一個匿名的 struct 以及對應的 trait 實作。文章深入解釋了閉包環境捕獲(by reference 實作 Fn、by mutable reference 實作 FnMut、by value 實作 FnOnce)與呼叫方式之間的關聯,讀完後會讓人真正體會到 Rust 所謂的「零成本抽象(zero-cost abstractions)」是多麼優雅地落實在型別系統之中。此外,在跨執行緒傳遞閉包時,編譯器自動推導的 Send + 'static 邊界更是並發與非同步程式設計的核心基石。
Option 的優雅模式:Rust: Using Options by example
在剛從其他語言轉來 Rust 時,許多人處理 Option<T> 的方式依然充滿傳統指令式語言的影子,滿滿的 if let Some(x) = ... 甚至動不動就 .unwrap()。
Ameya Lokare 這篇文章透過豐富且簡潔的程式碼範例,展示了如何活用 map、and_then、unwrap_or_else、filter 與 take 等豐富的組合子(combinators)。學會用函數式風格來操作 Option,是從「能寫出跑得動的 Rust」跨入「寫出 idiomatic 的 Rust」的重要轉折點。
非同步的基石:Futures 與 Tokio
- Futures · Tokio(註:官方文件現已更新為現代
async/await教學;而在 2019 年當時,非同步程式設計主要奠基於futures 0.1的組合子模型)
在 2019 年的這個時間點,Rust 2018 edition 已經釋出,而大家引頸期盼的 async/await 語法還在 nightly 版本中快速迭代(預計今年稍晚才會正式進入穩定版)。此時想要處理高併發網路 I/O,主流依然是透過 Tokio 與 futures 0.1 的組合子。
理解 Rust 的 poll-based future 模型非常關鍵:與 JavaScript 或 Go 不同,Rust 中的 Future 是被動惰性的(lazy),建立 Future 物件並不會在背景啟動執行緒,它必須被主動丟進 executor 透過 poll 方法驅動。這篇官方文件是掌握非同步本質必讀的敲門磚。
批判性回顧:Julio Merino 的 Rust Review 系列
前 NetBSD 開發者、當時任職於 Google 的 Julio Merino 撰寫了一系列針對 Rust 的全面評測。
這系列文章的可貴之處在於他不盲目推崇,而是從資深系統軟體工程師的角度,既盛讚 Rust 在型別安全與防範並行錯誤上的突破,也客觀剖析了學習曲線陡峭、編譯速度緩慢、以及依賴套件(Cargo crates)膨脹所帶來的維護考驗。讀一讀這類冷靜客觀的評論,有助於我們在架構選型時保持清醒。
語言內部的特殊待遇:Rust Tidbits: Box Is Special
Manish Goregaokar 的這篇小品揭露了 Box<T> 在編譯器眼中的特權。在標準的自訂智慧指標(如 Rc 或 Arc)中,因為 Deref trait 的簽名是 fn deref(&self) -> &Self::Target,你無法在解引用時把內部的值所有權移出(move out of deref)。
但是對於 Box<T>,編譯器特別給予了多重特例支援:除了允許直接寫 *b 取得內部值的所有權之外,還享有內建解構器、支援 Box<Self> 方法接收者(method receiver),以及不受孤兒規則限制(可在定義 T 的 crate 中為 Box<T> 實作外部 trait)。這篇文章用簡潔有趣的語調揭開了編譯器特權底下的秘密。
物件與設計模式:Builder Pattern、Trait Objects、Box 與 Rc
習慣了物件導向(OOP)繼承體系的工程師在初學 Rust 時,常常苦惱於不知道該如何組織程式碼結構。Alexandre Beslic(@abronan)這篇文章串聯了 Builder 模式、Trait Objects 的動態分派(Box<dyn Trait>),以及透過引用計數指標(Rc<T>)處理多重所有權的時機,對於建立良好的程式碼設計心智模型非常有幫助。
常見困惑與盲點解析
在學習 Rust 的過程中,有幾個看似微小卻極容易困惑的點,特別值得拿出來探討:
&foo 與 foo.as_ref() 的差異
剛學 Rust 時,常常會看到有人寫 &foo,有人卻寫 foo.as_ref(),這兩者有何本質差別?
簡而言之:
&foo是編譯器原生的取參照操作。若foo型別為Foo,在一般運算式中&foo產生&Foo(但在模式比對遇到&mut foo時,綁定模式亦可能推導出&mut Foo)。foo.as_ref()是呼叫AsRef<T>trait 定義的方法:fn as_ref(&self) -> &T。它能返回的目標型別不限於&Foo,而是可以取得任何實作了AsRef<T>的目標型別參照。
最經典的例子就是 String 與 &str:
let s = String::from("hello");
// ⚠️ 若不給予型別註記,直接呼叫會觸發編譯錯誤 error[E0283]:
// let r = s.as_ref();
// 原因在於 String 同時實作了 AsRef<str>、AsRef<[u8]>、AsRef<Path> 與 AsRef<OsStr>,
// 編譯器在缺乏目標型別線索時無法推導。
let r1: &String = &s; // 原生借用,型別為 &String
let r2: &str = s.as_ref(); // 明確指定目標型別,直接取得 &str
AsRef 本質上是在不消耗原始值所有權的前提下提供低成本的唯讀視圖(與消耗所有權做型別轉換的 Into 截然不同)。在撰寫通用函式 API 時,AsRef 展現了相當關鍵的靈活性:
fn print_length<T: AsRef<str>>(input: T) {
println!("長度: {}", input.as_ref().len());
}
// 呼叫者可以直接傳入 &str 或 String,完全不需要手動做格式轉換
print_length("world");
print_length(String::from("world"));另一個常見的應用是檔案路徑:AsRef<Path> 讓你的函式可以同時無縫接受 &str、String、&Path 或 PathBuf,搭配 Borrow<T> 與 ToOwned 構成 Rust 兼顧效能與人體工學的型別抽象體系。
到底在什麼情境下需要使用 Option::take()?
這是我在初學 Rust 時經常遇到的另一個痛點。
我們常會遇到這樣的情況:你擁有一個結構體的可變借用(&mut self),裡面有一個欄位是 Option<T>。在某個方法中,你想要取得裡面那個 T 的所有權,拿去傳給某個需要 ownership 的函式:
struct Worker {
state: Option<State>,
}
impl Worker {
fn transition(&mut self) {
// 編譯報錯:cannot move out of `self.state` which is behind a mutable reference (error[E0507])
// let old_state = self.state.unwrap();
}
}編譯器之所以嚴厲拒絕,是因為從 &mut self 參照中抽走資料會讓該記憶體位置處於未初始化(uninitialized)狀態,借用檢查器無法跨參照保證其有效性;此外若結構體實作了 Drop trait,任何欄位的部分移動更會直接觸發 error[E0509]。
這時 Option::take() 就是救星:
impl Worker {
fn transition(&mut self) {
// take() 會將值取出並回傳 Some(old_state),同時在原位置留下 None
if let Some(old_state) = self.state.take() {
let new_state = process(old_state);
self.state = Some(new_state);
}
}
}take() 的妙處在於它是單一的取值操作(底層為 mem::replace(self, None)):把值抽走的同時,順手塞入一個合法的 None。這樣既拿到了所有權,結構體內部在任何時刻也依然處於已初始化的合法狀態。
另一個經典場景是自訂鏈結串列(Linked List)或樹狀節點的析構。Rust 預設的遞迴 Drop 在遇到深層巢狀鏈結結構時,會因為呼叫堆疊過深而引發 stack overflow。透過在 Drop 實作中搭配 take() 進行迭代式釋放(iterative drop),能平坦化呼叫鏈:
struct List {
head: i32,
next: Option<Box<List>>,
}
impl Drop for List {
fn drop(&mut self) {
let mut cursor = self.next.take();
while let Some(mut node) = cursor {
// 每次迴圈切斷下一節點並接管其所有權,使前一節點在局部作用域平坦釋放
cursor = node.next.take();
}
}
}在日常開發中,相關的同族 API 還包括:mem::take(&mut val)(要求目標型別實作 Default)、mem::replace(&mut place, new_val) 以及 Option::insert(val) 等實用工具。
Go vs Rust:2019 年的雙城思維
身為 Go 語言的長期使用者,在深入學習 Rust 之後,無可避免地會面臨兩者的比較:
社群上常有人喜歡挑起語言之間的優劣爭論,但在我看來,這兩個語言在設計哲學上根本不是零和博弈,而是為了完全不同的目標進行極致優化:
解決問題的維度不同
- Go 追求的是開發者的心智簡潔與團隊一致性: Go 的語法極其精簡,刻意捨棄了許多複雜的高階抽象特性。搭配高速編譯、強大的內建工具鏈(gofmt、go test)、Goroutine 並發模型以及高效能垃圾回收(GC)。它讓任何水平的工程師都能在短時間內寫出風格一致、易於維護的後端微服務與網路工具。
- Rust 追求的是極致的控制力與記憶體絕對安全:
Rust 沒有 GC,執行時期負擔僅限於 allocator 與 panic 處理機制;在
no_std裸機或嵌入式環境下更可做到近乎零執行時期負擔。它把所有驗證壓力前置到編譯期,透過型別系統與借用檢查徹底杜絕資料競爭(data races)與記憶體安全隱患。它的型別表現力極為強大,但也伴隨著顯著的心智負擔與編譯成本。
如何選擇?
Matthias Endler 與 Julio Merino 的文章給出了非常中肯的指引:
- 選擇 Go 的情境:如果你要做的是一般的網路 API、微服務、內部 CLI 工具,或是需要帶領背景多元的團隊快速交付業務價值,Go 通常是更務實、性價比更高的選擇。
- 選擇 Rust 的情境:如果你在開發底層系統軟體(如 Fuchsia OS 的底層服務)、作業系統核心、資料庫引擎、瀏覽器渲染器、嵌入式系統,或是音訊與影像即時編解碼,任何 GC 停頓都會造成致命影響,且對記憶體 footprint 有極嚴苛要求的場景,Rust 是當前無可取代的現代化利器。
結語
從 Go 跨進 Rust 的過程,就像是習慣了自排車的便利後,重新學習開手排車——你必須自己思考離合器接合點、檔位與轉速比,一開始難免挫折手忙腳亂;但一旦開順了,那種每一滴引擎動力與機械運轉都在自己精確掌控之下的充實感,是前所未有的。
學習 Rust 不只是多掌握了一門語法,更是一趟重新反思記憶體管理、並發安全與型別抽象的思維重塑之旅。
參考資源清單
- The Rustonomicon:官方 Unsafe Rust 深度指南。
- Finding Closure in Rust:Huon Wilson 剖析閉包編譯機制的經典之作。
- Rust: Using Options by example:Ameya Lokare 的 Option 組合子模式實例。
- Futures · Tokio:Tokio 官方非同步與 Futures 入門文件。
- Rust Review Series:Julio Merino 從系統軟體工程師視角的深度評測。
- Rust Tidbits: Box Is Special:Manish Goregaokar 剖析 Box
編譯器特權。 - Rust, Builder Pattern, Trait Objects, Box
and Rc :Alexandre Beslic(@abronan)探討物件設計模式在 Rust 中的落實。 - Go vs Rust? Choose Go.:Matthias Endler 的技術選型理性分析。
- Rust vs. Go:Julio Merino 對 Rust 與 Go 實踐經驗的客觀對比。