起步:先把地基和你会的那门语言对齐

Cargo 与工程骨架

它在解决什么

Cargo 同时是四样东西:构建工具、包管理器、测试运行器、发布工具。 从 Maven / npm / go mod 过来的人几乎不需要适应期 —— 它的模型是熟悉的。

这一篇只讲三件真正会影响日常的事:生成的那几个文件是什么、 check / build / run 该用哪个、怎么不让编译器版本漂。

⚠️ 这一篇的结论大部分不是语言层面的,所以它们不在本站那套「示例真跑 rustc」的体系里。它们由另一道闸门(npm run test:cargo)每次部署前 真的跑一遍 cargo new / check / build / test 来验证 —— 下面出现的 每个文件名、每条输出都是那样得来的。

cargo new 到底生成了什么

实测只有三个文件:

demo/
├── .gitignore        ← 内容就一行:/target
├── Cargo.toml        ← 项目清单
└── src/main.rs       ← 入口

Cargo.toml 长这样:

[package]
name = "demo"
version = "0.1.0"
edition = "2024"

[dependencies]

⚠️ 没有 Cargo.lock。 它要等到第一次 cargo check 或 cargo build 才会生成。很多人以为 new 就该有,于是看到 git 里没有它会疑惑。

src/main.rs 里就是那三行 hello world。那个 fn main 不能改名 —— 可执行程序必须有它,改了就是 E0601。

📌 cargo new --lib 则不生成 main.rs 而是 lib.rs,也不需要入口 —— 库编不出可执行文件。

Cargo.toml 和 Cargo.lock 的分工

这一对和 package.json / package-lock.json、go.mod / go.sum 是同一个套路:

文件 谁写 内容 进不进 git
Cargo.toml 你 想要什么(serde = "1.0" 这种范围) 进
Cargo.lock Cargo 实际锁定到哪个版本 看情况

⚠️ 最后那一格是唯一需要判断的:做可执行程序就提交 Cargo.lock (你要的是「所有人构建出同一份东西」),做库就别提交 (下游要能和它自己的其它依赖协商版本)。

check / build / run 的分工

cargo check   类型检查、借用检查,全套 ——  但不生成机器码
cargo build   上面那些 + 真的编译链接    →  target/debug/demo
cargo run     build + 立刻跑一遍

实测确认过一件事:cargo check 之后 target/debug/demo 是不存在的, cargo build 之后才有。这就是它俩全部的差别。

⚠️ 广为流传的说法是「cargo check 比 cargo build 快很多」。这话方向对, 但在小项目上根本量不出来 —— 我在这个空项目上实测 check 0.11 秒、 build 0.07 秒,build 反而更快(增量缓存已经热了)。

⇒ 所以这里不给数字,只给机制:省掉的是代码生成和链接这两步, 项目越大、依赖越多,省的越明显。写代码时的循环用 check, 要跑起来时才 build。

把编译器版本钉死

Rust 每六周发一个稳定版,而错误码的措辞、新 lint、甚至某些行为都会跟着变。 在项目根放一个 rust-toolchain.toml 就能钉住:

[toolchain]
channel = "1.98.1"

之后任何人在这个目录里跑 cargo,rustup 都会自动切到这个版本 (没装就先装)。

⚠️ 有个容易踩空的细节:rustup 是从「当前目录」往上找这个文件的, 不是从项目根找。所以在项目外面跑的命令完全不受它约束 —— 实测过一次:仓库根放一个 channel = "1.70.0" 的文件, 在仓库里 rustup show active-toolchain 给的是 1.70.0, 换到 /tmp 里同一条命令给的还是 default。

⇒ 如果你有脚本在临时目录里调 rustc(比如做代码校验), 那个文件管不到它 —— 得另想办法把版本记下来。

⭐ 这件事对写教程尤其要紧:本站所有 Rust 示例的断言(编译成败、错误码、 输出)都是在一个固定版本上跑出来的,闸门每次都会把 rustc --version 打进日志 —— 「我到底在哪个实现上验的」本身就是结论的一部分。

target/ 里是什么,以及它为什么那么大

target/debug/       未优化 + 调试信息 + 溢出检查
target/release/     优化 + 无调试信息 + 无溢出检查

两者语义不完全相同,这是 Rust 少见的一处「编译参数改变行为」: debug_assert! 在 release 下被整个编译掉, 整数溢出也从 panic 变成回绕(见《为什么是 Rust》那张表)。

⇒ 判据:debug_assert! 用来写「这个不变量我很确定」的检查 (发布版里代价为零),必须永远检查的东西用 assert!。

target/ 动辄几个 G 是正常的 —— 它存了每个依赖的每种配置的产物。 .gitignore 里那一行 /target 就是为它准备的。

下一步

工具就位了,接下来一口气把基础语法过一遍 —— 见《语法速通:给已经会编程的人》: 只讲和你已经会的那门语言不一样的地方,不讲「什么是循环」。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

`cargo new` 生成的那三行,为什么必须有 `fn main`

fn main() {
    println!("Hello, world!");
}

编译通过 · 输出 "Hello, world!\n"

对照:把 `main` 改个名字
fn start() {
    println!("Hello, world!");
}

编译不过:error[E0601]

对照项报 `E0601`:可执行程序必须有一个叫 `main` 的入口。⚠️ 库(`cargo new --lib`)不需要 —— 那种 crate 编不出可执行文件,自然也不需要入口。

`debug_assert!` 只在 debug 下生效

fn main() {
    let n = 1;
    debug_assert!(n == 2, "n 应该是 2,实际是 {n}");
    println!("跑完了");
}

编译通过,但运行时 panic · 退出码 101

运行时说了什么

thread 'main' panicked at cargo-debug-assertions.rs:3:5:
n 应该是 2,实际是 1
对照:把条件改成成立的
fn main() {
    let n = 2;
    debug_assert!(n == 2, "n 应该是 2,实际是 {n}");
    println!("跑完了");
}

编译通过,输出:"跑完了\n"

和整数溢出是同一个机制:`debug_assert!` 在 `cargo build --release` 下会被整个编译掉,一行代码都不剩。⇒ 用它写「这个不变量我很确定」的检查,代价在发布版里是零;而必须永远检查的东西要用 `assert!`。