起步:先把地基和你会的那门语言对齐
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!`。