Community编程与开发github.com

teneum/rust-dev

Day-1 指南,助你高效使用 Rust 构建项目——涵盖所有权、错误即值、String 与 &str、Box/Rc/Arc、anyhow 与 thiserror 的选择,以及关键 crate 推荐(tokio、serde、axum、sqlx)。在启动 Rust 项目、与借用检查器斗争或挑选 crate 时使用。

rust-dev 是什么?

这是一个面向 Rust 开发初学者的 Day-1 上手指南,帮助开发者从第一天起就写出高质量的 Rust 应用。它并非一本全面的教科书,而是专注于核心概念和实用决策。该技能涵盖了 Rust 的五个核心思想(所有权、别名与可变性互斥、错误即值、trait 不是 Java 接口、借用检查器是设计诊断器),并为每个函数签名提供了“拥有、读取、修改”的判断框架。此外,它还包含一个决策表,用于指导 String 与 &str、Vec<T> 与 &[T]、Box<T> 与 Rc<T> 与 Arc<T>、anyhow 与 thiserror 等关键选择,并附带了从 Python、JavaScript、Go、Java/C#、C++ 等语言迁移时的常见陷阱。该指南还提供了一组核心 crate 推荐(如 serde、tokio、axum、sqlx)以及一组应该从一开始就内化的 Rust 惯用写法(如 ? 操作符、迭代器链、match 穷尽性)。该项目可在任何 Rust 开发环境中使用(CLI、服务、库),并强调了 rust-analyzer 和 cargo check/run/test/clippy/fmt 等工具的使用。

兼容平台~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 开发 - Day 1

一个实用的基础指南,助你从第一个提交起就写出优秀的 Rust 应用。这不是一本教科书。它专注于 Rust 与其他语言的差异、决定后续一切的第一天决策,以及覆盖大多数真实应用的小型 crate 集合。

何时使用

  • 启动一个新的 Rust 项目(CLI、服务、库)
  • 从 Python、JavaScript、Go、Java/C# 或 C++ 转向 Rust
  • 在拥有/借用类型、智能指针、trait 对象与泛型之间做出选择
  • 选择错误处理策略(anyhow 与 thiserror)
  • 决定使用哪些 crate
  • 配置一个最小但意见明确的 Cargo.toml、clippy 和 rustfmt

Day-1 设置

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

# 2. 确认组件(rustfmt 和 clippy 随稳定版一起提供,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      # 构建并运行测试(包括文档测试)
cargo clippy --all-targets   # 代码检查,包括测试和基准(在推送前运行)
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() 创建一个深拷贝。两者都拥有自己的副本。

当所有者离开作用域时,值被丢弃(内存释放、文件关闭、锁释放)。没有垃圾回收,没有手动的 free。这就是 RAII,由编译器强制实施。

2. 别名与可变性互斥

在任何时刻,一份数据要么:

  • 一个可变引用(&mut T),要么
  • 任意数量的不可变引用(&T),

两者不能同时存在。这一条规则消除了数据竞争和大多数 use-after-free 错误。借用检查器强制实施它。当它报错时,它是在告诉你你的数据所有权故事不清楚,而不是语言本身在找麻烦。

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 这些 trait。到目前为止还比较熟悉。不同之处:

  • 默认是静态分发。 当你编写 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 是最常见的新手陷阱,它会导致生命周期注释级联到所有包含你的结构体的类型中。

Day-1 决策表

对首先出现的选择给出的一行答案。

决策默认值何时选择另一个
String 与 &str(结构体字段)String几乎永远不是 &str,除非你有真正的理由并理解生命周期
String 与 &str(函数参数)&str仅当必须在函数内部拥有/存储它时使用 String
Vec<T> 与 &[T](参数)&[T]仅当必须拥有时使用 Vec<T>
Box<T> 与 Rc<T> 与 Arc<T>Box<T>(单个所有者,堆)Arc<T> 用于跨线程共享所有权。避免默认使用 Rc<T>;使用 Arc<T> 这样当你转向异步时就不需要重构
RefCell<T> 与 Mutex<T>Mutex<T>(或 RwLock<T>)同样原因:在 async/线程中工作,而 RefCell 不行
Option<T> 与 Result<T, E>Option<T> 用于“没有值”,Result<T, E> 用于“因某个原因失败”如果缺失携带了调用者应处理的意义,使用 Result
dyn Trait 与 impl Trait / <T: Trait>泛型(<T: Trait> 或 impl Trait)- 静态分发当你需要一个异构集合时使用 Box<dyn Trait>(Vec<Box<dyn Animal>>)
应用代码中的错误所有地方都使用 anyhow::Result<T>-
库代码中的错误thiserror 派生的枚举永远不要在公共库 API 中使用 Box<dyn Error> - 强制调用者向下转换
&self 与 &mut self 与 self&self 用于 getter,&mut self 用于 setter,self 用于构建器/消费操作-
模块布局内联模块直到文件变长,然后拆分一个模块 = 一个文件是 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 用于常见的单臂匹配。

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 守卫可以在 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:

  • let b = a; 对于堆上的值(如 String、Vec)是移动它。a 不再可用。使用 &a 借用或 a.clone() 复制。
  • 没有 null。Option<T> 是强制的。
  • 没有异常。Result<T, E> 和 ?。编译器不会让你忽略错误。
  • 没有继承。组合 + trait + 枚举。
  • 变量默认是不可变的。添加 mut 来改变。引用同理:& 与 &mut。
  • 重新声明一个名称是允许且惯用的:let input = input.trim(); 用新绑定隐藏旧绑定,可能类型不同。这不是修改,这是 Rust 代码就地转换值而不发明 input2 的方式。
  • 整数类型是显式的,索引需要 usize。它们的溢出行为也因构建而异:调试“包括导致程序在运行时_panic_ 的整数溢出检查”,而 --release 取消它们并执行“二进制补码环绕”。一个通过的测试套件在生产中可能仍然会溢出,因此在算术可能达到边界的地方使用 checked_*/saturating_*/wrapping_*,而不是依赖调试 panic 来发现它。strict_add 及其变体(自 1.91 稳定)在所有构建中 panic 溢出,而 [profile.release] 下的 overflow-checks = true 会在生产中保留调试行为。并且 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>。
  • 没有垃圾回收和 goroutine:所有权 + 借用,使用 tokio 的 async/await。异步模型是协作式的(await 是显式的让步点),而不是抢占式的。
  • interface{} 变成 trait。默认使用泛型进行静态分发;Box<dyn Trait> 仅在需要时使用。
  • 默认是静态链接。二进制文件更大但自包含。
  • panic! 应保留给应用代码中不可恢复的错误;不要将其用作 Go 风格的“记录日志并继续”。

来自 Java 或 C#:

  • Trait 不是默认具有虚分发的接口。<T: Trait> 是单态化的。dyn Trait 是可选的动态版本。
  • 没有 null 引用。Option<T>。
  • 没有异常。Result<T, E> 和 ?。
  • 没有类继承。对和类型使用枚举,对共享行为使用 trait。
  • 没有垃圾回收:所有权和借用决定生命周期。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,线程安全)。
  • 宏是卫生的。过程宏(派生、属性、类函数)是 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)],自动生成 --help
axumWeb 框架(路由器、中间件、提取器)。与 tokio 和 tower 生态配合
reqwestHTTP 客户端(同步和异步)。发出 HTTP 请求
sqlx异步 SQL。编译时检查的查询,支持 PostgreSQL/MySQL/SQLite,无需 ORM
tokio-rs/console异步诊断。tokio-console 显示每个任务的 CPU/等待时间和状态
tracing结构化日志和跨度。tracing-subscriber 用于输出格式化
chrono日期和时间。NaiveDateTime、Utc、时区
uuidUUID。Uuid::new_v4()、Uuid::parse_str()

何时避开流行 crate

  • lazy_static:使用 std::sync::LazyLock(自 1.80 稳定)或 once_cell::sync::Lazy 代替。
  • cfg-if:使用 cfg_select!(1.95 稳定)代替。
  • anyhow 用于库:thiserror 是你的朋友。保持公共 API 中的错误具体。
  • actix-web 用于新的 Rust 开发者:axum 是更标准的异步生态的一部分,减少了认知开销。

最小且意见明确的 Cargo.toml

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

[dependencies]

[dev-dependencies]

[profile.release]
# 默认情况下,发布版本去掉 panic 栈展开(更小的二

Individual skills in this repo

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

相关技能