Community程式設計與開發github.com

tenequm/rust-dev

從第一天開始良好地建構 Rust 專案的指南 — 涵蓋所有權、錯誤作為值、String vs &str、Box/Rc/Arc、anyhow vs thiserror 以及精選 crate 清單 (tokio, serde, axum, sqlx)。適合在啟動 Rust 專案、對抗借用檢查器或挑選 crate 時使用。

rust-dev 是什麼?

這個技能是一個務實的 Rust 開發入門指南,專為從第一天起就能寫出好程式而設計,而非成為一本厚重的教科書。它專注於那些從第一天就會影響整個專案的關鍵決策,例如正確選擇所有權策略(String vs &str、Box/Rc/Arc)、錯誤處理模式(anyhow 用於應用程式程式碼,thiserror 用於函式庫),以及一個精選的 crate 清單(tokio、serde、axum、sqlx),這些 crate 涵蓋了大多數實際應用程式的需求。 此技能明確支援 Claude Code、VS Code、Zed、Neovim 和 Helix 等編輯器,並強調安裝 rust-analyzer 的必要性,以獲得即時的 lint 反饋。它透過一個「5 個概念的心智模型」來解釋 Rust 的根本差異,包括所有權、資料競爭預防、錯誤作為值、trait 語義以及將借用檢查器視為設計指引。 該指南還包含一個決策表,針對 String 與 &str、Vec 與 &[T]、Box 與 Arc、Option 與 Result 等常見取捨提供具體建議。此外,它還為來自 Python、JavaScript、Go、Java、C# 和 C++ 的開發者提供了轉換指引,指出了他們可能遇到的最大陷阱。對於任何想要有效率地學習 Rust 並避免常見錯誤的開發者來說,這是一個不可或缺的資源。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Rust 開發 - 第一天

一個務實的基礎,讓你從第一次提交就能寫好 Rust 應用程式。這不是教科書。重點在於與其他語言的差異、那些從第一天就會影響一切形狀的決策,以及涵蓋大多數實際應用程式的小型精選 crate 清單。

何時使用

  • 啟動一個新的 Rust 專案(CLI、服務、函式庫)
  • 從 Python、JavaScript、Go、Java/C# 或 C++ 轉向 Rust
  • 在擁有/借用類型、智慧指標、trait 物件與泛型之間做選擇
  • 挑選錯誤處理策略(anyhow vs thiserror)
  • 決定要使用哪些 crate
  • 配置一個最小但具有主觀意見的 Cargo.toml、clippy 和 rustfmt

第一天設定

# 1. 安裝工具鏈 (rustup 是工具鏈管理器)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 確認元件 (rustfmt 和 clippy 隨 stable 版本提供,rust-src 啟用 IDE 功能)
rustup component add rustfmt clippy rust-src

# 3. 建立專案
cargo new my-app          # 二進位 (src/main.rs)
cargo new --lib my-lib    # 函式庫 (src/lib.rs)

# 4. 開發循環 (記住這四個)
cargo check     # 快速型別檢查,不產生程式碼
cargo run       # 建置並執行 (二進位)
cargo test      # 建置並執行測試 (包含 doctest)
cargo clippy --all-targets   # lint,包含 tests/ 和 benches/ (推送前執行)
cargo fmt       # 格式化

# 5. 管理相依性,不需手動編輯 Cargo.toml
cargo add tokio --features full
cargo remove tokio
cargo update                 # 在現有 semver 範圍內重新計算 Cargo.lock

cargo update 只會在 Cargo.toml 中已有的版本範圍內移動。跨越主版本(1.x 到 2.0)需要編輯 Cargo.toml 或使用 cargo add <crate>@2。

rust-analyzer 是必需的。 它是每個編輯器都會使用的語言伺服器(VS Code、Zed、Neovim、Helix,RustRover 使用自己的引擎但功能相當)。在 VS Code 中,安裝 rust-analyzer 擴充功能,並將 rust-analyzer.check.command 設定為 "clippy",這樣就能在儲存時獲得 lint 反饋。

想要之後使用檔案監看器? 執行 cargo install --locked bacon,然後在你的專案中執行 bacon。第一天不需要。

Rust 心智模型:5 個概念

Rust 用兩件事交換了你在大多數語言中視為理所當然的東西(垃圾回收器和例外):編譯時關於記憶體、資料競爭和錯誤處理的保證。語言的形狀來自這個交易。

1. 所有權:每個值都只有一個擁有者

把值想成實體對象。一本書、一個檔案、一個網路連線。在任何時刻,有一個變數擁有它。你可以:

  • 移動它:let b = a; 將所有權交給 b。a 消失了。
  • 不可變地借用它:&a 讓其他人查看它。允許多個讀者。
  • 可變地借用它:&mut a 允許一個人修改它。獨占存取。
  • 克隆它:a.clone() 建立一個深拷貝。兩者都擁有自己的副本。

當擁有者離開作用域時,該值被丟棄(記憶體釋放、檔案關閉、鎖定釋放)。沒有 GC,沒有手動的 free。這就是 RAII,由編譯器強制執行。

2. 別名異或可變性

在任何時刻,一塊資料具有要麼:

  • 一個可變引用(&mut T),要麼
  • 任意數量的不可變引用(&T),

但不能同時存在。這條規則消除了資料競爭和大多數釋放後使用錯誤。借用檢查器強制執行它。當它抱怨時,它是在告訴你你的資料所有權故事不夠清晰,而不是語言在為難你。

3. 錯誤是值,不是例外

沒有 try/catch。可能失敗的函式返回 Result<T, E>。可能不返回有用東西的函式返回 Option<T>。編譯器強制你處理這兩者。? 運算子用一個字元將錯誤向上傳播到呼叫堆疊:

fn read_config() -> Result<Config, anyhow::Error> {
    let text = std::fs::read_to_string("config.toml")?;  // ? = 在 Err 時提前返回
    let config = toml::from_str(&text)?;
    Ok(config)
}

沒有 null。Option<T> 是 None 或 Some(value)。編譯器不會讓你忘記 None 的情況。

4. Trait 不是 Java 介面

trait 定義行為。類型 impl traits。到目前為止還算熟悉。不同之處在於:

  • 靜態分派是預設的。 當你寫 fn f<T: Display>(x: T) 時,編譯器會為你呼叫的每個具體 T 產生一個獨立的 f 副本(單態化,就像 C++ 模板)。執行時期零開銷。
  • 動態分派是選擇加入的,透過 dyn Trait(通常是 Box<dyn Trait> 或 &dyn Trait)。每次呼叫有一次 vtable 查找。
  • 沒有繼承。 Trait 可以組合。如果你發現自己為了「擴展」一個類型而使用 Deref,停下來改用組合或列舉。
  • 孤兒規則:你可以為別人的類型實作自己的 Trait,或為自己的類型實作別人的 Trait,但不能兩者都是外部的。這保持了相依性解析的合理性。

5. 借用檢查器是一個設計預言

新進者最常見的錯誤是將編譯器錯誤視為需要靜音的障礙。它們不是。幾乎每一個借用檢查錯誤都揭露了一個關於誰擁有什麼的真實問題。當你卡住時,問題很少是「我如何讓這個編譯通過」,而幾乎總是「我這裡想要的實際所有權關係是什麼?」閱讀錯誤訊息。編譯器異常地資訊豐富。

每個函式簽章的 3 個問題

在寫函式之前,問:它需要擁有、讀取還是修改輸入?

fn consume(s: String)        // 擁有:    函式接管責任,呼叫者失去它
fn read(s: &str)             // 讀取:   函式查看它,呼叫者保留它
fn modify(s: &mut String)    // 修改:   函式就地改變它

90% 情況下適用的預設值:

  • 函式參數:偏好 &str 勝過 String,偏好 &[T] 勝過 Vec<T>(這些是切片,可以接受擁有和借用的呼叫者)。
  • 函式返回值:返回擁有類型(String、Vec<T>)。返回引用意味著生命週期;在需要它們之前避免使用。
  • 結構體欄位:偏好擁有類型(String、Vec<T>)。在結構體中儲存 &str 是新進者最常見的陷阱,它會將生命週期註解級聯到所有持有你結構體的類型。

第一天決策表

對於首先出現的選擇,提供一行答案。

決策預設值何時選擇另一個
String vs &str (結構體欄位)String幾乎永遠不要用 &str,直到你有一個真正的理由並且理解生命週期
String vs &str (函式參數)&str只有當你必須在內部擁有/儲存它時才使用 String
Vec<T> vs &[T] (參數)&[T]只有當你必須擁有時才使用 Vec<T>
Box<T> vs Rc<T> vs Arc<T>Box<T> (單一擁有者,堆積)Arc<T> 用於跨執行緒的共享所有權。避免預設使用 Rc<T>;使用 Arc<T> 這樣當你轉向非同步時就不需要重構
RefCell<T> vs Mutex<T>Mutex<T> (或 RwLock<T>)同樣原因:在 async/執行緒中工作,而 RefCell 不行
Option<T> vs Result<T, E>Option<T> 用於「沒有值」,Result<T, E> 用於「因某種原因失敗」如果缺席傳達了呼叫者應該處理的意義,則使用 Result
dyn Trait vs impl Trait / <T: Trait>泛型 (<T: Trait> 或 impl Trait) - 靜態分派當你需要一個異質集合時使用 Box<dyn Trait> (Vec<Box<dyn Animal>>)
應用程式程式碼中的錯誤處處使用 anyhow::Result<T>-
函式庫程式碼中的錯誤thiserror 衍生的列舉永遠不要在公開函式庫 API 中使用 Box<dyn Error> - 這會強迫呼叫者進行 downcast
&self vs &mut self vs self&self 用於 getter,&mut self 用於 setter,self 用於 builder/消耗性操作-
模組佈局內聯模組,直到檔案變長,然後拆分一個模組 = 一個檔案是 Java/C# 的直覺,不是 Rust 的

需要儘早內化的慣用寫法

這些幾乎出現在每個 Rust 程式中。在第一週學會它們。

? 用於錯誤傳播。 用一個字元取代九行 match。

let body = reqwest::get(url).await?.text().await?;

迭代器鏈勝過手動迴圈。 編譯成與手寫迴圈相同的機器碼(LLVM 內聯閉包)。慣用的 Rust 在風格上是函數式的。

let active_emails: Vec<String> = users
    .iter()
    .filter(|u| u.active)
    .map(|u| u.email.clone())
    .collect();

match 的窮盡性。 為一個列舉添加一個新變體,而不處理它的每個 match 都會變成編譯錯誤。善用這個。這是任何語言中最強大的重構工具之一。

讓編譯器驅動重構。 同樣的技巧也適用於結構體:添加一個沒有 Default 的必填欄位,每個現有的建構子都會變成編譯錯誤 - 這是一個免費且窮盡的、需要更新的所有位置的工作清單。

if let 和 let else 用於常見的單臂 match。

if let Some(name) = user.name { println!("hi {name}"); }

let Some(name) = user.name else { return Err(anyhow!("no name")); };
// 從這裡開始 `name` 在作用域內,沒有巢狀

From / Into 用於類型轉換。 實作 From,免費獲得 Into。? 使用 From 自動轉換錯誤類型。

Option / Result 上的組合子。 在求助於 match 之前,先嘗試 .map、.and_then、.unwrap_or、.unwrap_or_else、.ok_or。

衍生意義巨集。 #[derive(Debug, Clone, PartialEq)] 可以免費獲得 80% 的樣板程式碼。添加 #[derive(Serialize, Deserialize)] 用於 JSON。

最近的語法糖。 值得了解的已穩定功能,按最新排序:bool::ok_or 和 ok_or_else (1.98) 將條件直接變成 Result (ready.ok_or(Error::NotReady)?)。cfg_select! 是一個在 cfg 謂詞上的編譯時 match,取代了 cfg-if crate 的大部分用途,而 if let guards 作用於 match 分支 (match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... }) - 兩者都是 1.95。assert_matches! 和 debug_assert_matches! (1.96) 是對模式而不是相等性進行斷言,這是列舉的自然斷言方式。你會經常看到的兩個較舊功能,不要誤以為它們是奇特的:let-chains (if let Some(x) = a && x > 3 { ... }),自 1.88 起在 edition 2024 中穩定;以及非同步閉包 (async || { ... },帶有 AsyncFn 系列約束),自 1.85 起穩定。

從 X 語言過來,這裡是會坑你的地方

從 Python 或 JavaScript 過來:

  • 對於堆積上的值(如 String、Vec),let b = a; 移動了它。a 不再可用。請用 &a 來借用,或 a.clone() 來複製。
  • 沒有 null。Option<T> 是強制性的。
  • 沒有例外。Result<T, E> 和 ?。編譯器不允許你忽略錯誤。
  • 沒有繼承。組合 + traits + 列舉。
  • 變數預設是不可變的。添加 mut 來使其可變。引用也是如此:& vs &mut。
  • 重新宣告一個名稱是允許且慣用的:let input = input.trim(); 隱藏了舊的繫結,可能具有不同的類型。這不是可變性,這就是 Rust 程式碼就地轉換值而不發明 input2 的方式。
  • 整數類型是明確的,索引需要 usize。它們的溢出行為也根據建置而不同:debug 模式「包含整數溢位檢查,這會導致你的程式在執行時期 恐慌」,而 --release 模式則移除它們並執行「二的補數包裝」。一個通過的測試套件仍然可能在生產環境中包裝,因此在那些算術可能實際到達邊界的地方使用 checked_*/saturating_*/wrapping_*,而不是依賴 debug 恐慌來發現它。strict_add 及其同類函式(自 1.91 起穩定)在每次建置中都會在溢位時恐慌,而 [profile.release] 下的 overflow-checks = true 則在生產環境中保留 debug 行為。此外,as 在整數類型之間會默默地截斷(300_u32 as u8 是 44)- 在任何值可能不適合的地方使用 u8::try_from(x)?。
  • String 是不可索引的。s[0] 無法編譯 - 請參閱 references/ownership-and-types.md 了解應該用什麼替代。

從 Go 過來:

  • 錯誤作為值 - 相同的直覺,但改用 ? 而不是 if err != nil。
  • 沒有 nil。Option<T>。
  • 沒有 GC 也沒有 goroutine:所有權 + 借用,使用 tokio 的 async/await。async 模型是協作式的(await 是一個明確的讓出點),而不是搶佔式的。
  • interface{} 變成 trait。預設使用泛型進行靜態分派;只在需要時使用 Box<dyn Trait>。
  • 靜態連結是預設的。二進位檔更大但自足。
  • panic! 應該保留給應用程式程式碼中無法恢復的錯誤;不要像 Go 的「記錄並繼續」那樣使用它。

從 Java 或 C# 過來:

  • Trait 不是預設具有虛擬分派的介面。<T: Trait> 是單態化的。dyn Trait 是選擇加入的動態版本。
  • 沒有 null 引用。Option<T>。
  • 沒有例外。Result<T, E> 和 ?。
  • 沒有類別繼承。使用列舉作為 sum 類型,使用 trait 作為共享行為。
  • 沒有 GC:所有權和借用決定生命週期。Arc<T> 是最接近 Java 引用的東西。
  • 泛型是單態化的,而不是類型擦除。

從 C++ 過來:

  • 就像 RAII,但借用檢查器在編譯時強制執行它。
  • 沒有拷貝/移動建構子。Clone 是明確的,而 Copy 是一個用於廉價逐位複製的標記 trait。
  • 在安全程式碼中沒有未定義行為(理論上)。
  • & 是一個編譯時檢查的借用,而不是裸指標。裸指標存在(*const T、*mut T)但需要 unsafe 才能解引用。
  • 智慧指標包括 Box<T> (unique_ptr)、Rc<T> (shared_ptr,單執行緒)、Arc<T> (shared_ptr,執行緒安全)。
  • 巨集是衛生的。程序式巨集(derive、attribute、function-like)是 serde、tokio::main 等工作方式的基礎。

精選 Crate 清單

這些涵蓋了大多數實際應用程式。按需添加;它們並非全部必需。

Crate它給你什麼
serde + serde_json序列化。#[derive(Serialize, Deserialize)] 就完成了
tokio非同步執行時期。#[tokio::main]、tokio::spawn、非同步 I/O
anyhow應用程式錯誤類型。anyhow::Result<T>、bail!、context()
thiserror函式庫錯誤列舉。#[derive(thiserror::Error)]
clapCLI 參數解析。#[derive(Parser)]
rayon資料並行性。將 .iter() 替換為 .par_iter() 以獲得並行性
reqwestHTTP 客戶端。非同步、TLS
axumWeb 框架。基於 tokio 和 tower 生態系統
sqlx非同步 SQL。編譯時檢查的查詢、遷移、Postgres/MySQL/SQLite
tokio-tungstenite + futures-utilWebSocket。tokio-tungstenite 處理連線協定,futures-util 上的 StreamExt / SinkExt 處理訊息
chrono日期時間。DateTime<Utc>、期間等
serde_qs + serde_with查詢字串序列化和反序列化,以及欄位層級的序列化修飾詞
tower / tower-http中介軟體。tower:服務抽象;tower-http:HTTP 專用中介軟體(壓縮、CORS、追蹤、請求/回應記錄)
tracing紀錄。tracing-subscriber 進行設定。#[instrument] 自動追蹤函式進入/離開
tempfile測試用的暫存檔案和目錄。NamedTempFile、TempDir
similar差異/編輯距離。用於工具中的差異和變更檢測
divan基準測試。#[divan::bench],漂亮的輸出,強大的比較功能
strum列舉的輔助 derive 巨集。Display、EnumString、AsRefStr、IntoStaticStr、EnumIter、EnumCount、EnumDiscriminants、EnumMessage、EnumProperty、EnumTable
serde_yaml如果 configuration 是 YAML 格式。否則預設使用 TOML;serde_json 甚至更快
tomlTOML 序列化和反序列化。透過 serde
uuidUuid::new_v4()、Uuid::parse_str()
regexRegex::new(r"^...$")?。編譯時期檢查?regex crate 本身不行,但 Regex::new 在 const 上下文中是允許的;或者使用 regex-lite 或 once_cell/LazyLock 進行延遲初始化

關於錯誤處理的特別說明

anyhow 和 thiserror 解決不同的問題。在應用程式程式碼中,使用 anyhow::Result<T>,這樣你就可以用 ? 傳播任何實現 std::error::Error 的類型,並用 .context() 豐富錯誤訊息。在函式庫程式碼中,導出一個由 thiserror 衍生的具體錯誤類型;這讓呼叫者可以在 match 中使用你的錯誤,而無需 downcast。將它們兩者配對使用是慣例:函式庫導出 thiserror 類型,應用程式使用 anyhow 包裝它們。

// 函式庫
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse error at line {line}: {msg}")]
    Parse { line: usize, msg: String },
}

// 應用程式
fn load() -> anyhow::Result<Config> {
    let raw = std::fs::read_to_string("config.toml")?;          // 自動轉換 Io
    let cfg: Config = toml::from_str(&raw).context("invalid config")?; // 添加上下文
    Ok(cfg)
}

最低 Cargo.toml

這些是你想要在專案中設定的 clippy 和 rustfmt 設定。

[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
rust-version = "1.85"

[dependencies]
# 一組很好的預設值:
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

[lints.clippy]
cargo = "warn"
pending = "deny"         # 你可以在推送到 CI 之前保留待辦事項
style = "warn"            # Clippy 的風格類別設為 warn(預設)而不是 allow "。
# optional: unwrap_used = "deny"          # 對 unwrap 零容忍
# optional: panic = "deny"                # 對 panic 零容忍
# optional: todo = "deny"                 # 在 CI 中防止合併 todo!()

[profile.release]
# 限制優化以保持更快的建置,除非你需要最大吞吐量
# debug = 1 給你良好的回溯訊息
codegen-units = 1
lto = "fat"
debug = 1
# 保留 debug 建置的整數溢出行為
overflow-checks = true

edition、rust-version 和 MSRV

  • edition 控制語法(2015、2018、2021、2024)。2024 edition 在 Rust 1.85 中穩定;現在大多數新的 crate 都使用 2021 以獲得廣泛相容性,並在準備就緒時遷移到 2024。
  • rust-version 設定最低支援的 Rust 版本(MSRV)。這會告訴工具鏈,此軟體包在舊版 Rust 上無法建置。它可以是一個完整的 semver 版本,如 "1.85"。對於函式庫來說至關重要;對於應用程式來說,它是可選的(而且一般是重新評估的)。
  • 如何選擇:如果你以 Rust 的最新穩定版本為目標,請省略 rust-version 以獲得最大的靈活性。如果你正在發布一個函式庫,請設定一個你所針對和測試的具體 MSRV。

[lints] 表格(Rust 1.74+)

[lints.clippy] 表格讓你可以將 clippy 配置保留在你的 Cargo.toml 中,而不是 .clippy.toml 或 #![allow(clippy::...)] 註解。上面的設定給了你好慣例。隨著時間的推移,針對你的領域進行調整。

CLI 專案的快速入門

cargo new my-cli && cd my-cli
cargo add clap --features derive
# 在你的 src/main.rs 中:
# use clap::Parser;
# /// 你的 CLI 文件字串
# #[derive(Parser)]
# struct Cli { ... }
# fn main() { let cli = Cli::parse(); ... }

非同步服務的快速入門

cargo new my-service && cd my-service
cargo add tokio --features full
cargo add axum serde serde_json
cargo add tower-http --features cors,compression-gzip,trace
cargo add tracing tracing-subscriber
cargo add sqlx --features runtime-tokio-rustls,postgres

tracing 設定:

// 在你的 main 函式頂部
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

tracing_subscriber::registry()
    .with(tracing_subscriber::fmt::layer())
    .with(tracing_subscriber::EnvFilter::from_default_env())
    .init();

需要了解的資源

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

相關技能