为什么需要 Cargo
在 Rust 中,一个库或可执行程序叫 crate,由编译器 rustc 编译。学 Rust 的第一个程序 “Hello, world!” 可以直接用 rustc 编译:
fn main() { println!("Hello, world!");}$ rustc hello.rs$ ./helloHello, world!但直接使用 rustc 有两个问题:
- 命令不统一。 你必须显式指定文件名
hello.rs,编译不同程序需要不同命令行,加上编译参数和外部依赖后命令会更复杂。 - 依赖难管理。 正经项目几乎都会用外部库,外部库又传递依赖别的库。手工获取所有依赖的正确版本并保持更新,既困难又容易出错。
因此 Rust 在 crate 之上引入了更高层的抽象——package(包),以及配套的包管理器 Cargo。
Cargo 是什么
Cargo 是 Rust 的包管理器。 它让包声明自己的依赖,并保证可重复的构建:无论在什么机器、什么时间构建,结果都一致可预期。
为此 Cargo 做四件事:
- 引入两个元数据文件:
Cargo.toml(包的元信息与依赖声明)和Cargo.lock(锁定依赖的精确版本)。 - 自动从 registry(如 crates.io)拉取并构建依赖。
- 以正确的参数调用
rustc,把依赖安排进构建。 - 引入约定,让使用 Rust 包更简单。
其中约定最有价值:Cargo 把命令标准化了。 无论项目叫什么、是库还是可执行程序,构建命令都是同一条:
$ cargo build所以并不夸张地说:学会构建一个 Cargo 项目,就学会了构建所有 Cargo 项目。
小结
Cargo 把 Rust 项目从”手工编译 + 手工管理依赖”变成”声明依赖 + 统一命令 + 可重复构建”的工程化状态。接下来我们动手:安装 Cargo,创建第一个项目。
安装 Cargo
Cargo 随 Rust 一起安装,不需要单独装。官方推荐用 rustup 安装 Rust:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh装好后运行 rustc --version 和 cargo --version,能看到版本号就说明安装成功。
rustup 负责管理 Rust 工具链和版本,以后升级就运行 rustup update。
创建第一个项目
用 cargo new 创建:
$ cargo new hello_world --bin$ cd hello_world--bin 表示创建二进制程序(可执行文件);创建库用 --lib,不传时默认 --bin。另外 cargo new 默认会初始化一个 git 仓库,不想要可以加 --vcs none。如果代码已经在一个目录里,用 cargo init 初始化(它不会新建目录)。
生成的结构:
.├── Cargo.toml└── src └── main.rsCargo.toml 是包的 manifest(清单),TOML 格式,Cargo 编译所需的所有元数据都在里面:
[package]name = "hello_world"version = "0.1.0"edition = "2024"
[dependencies]name/version:包名和版本(semver)edition:使用的 Rust 版本规范(2024 是目前最新),影响语法和行为,保持默认即可[dependencies]:依赖列表,下面会讲
src/main.rs 里是 Cargo 生成的 hello world 程序。
构建与运行
$ cargo build$ cargo runcargo build:编译,产物在target/debug/cargo run:编译并运行,一步到位cargo check:只做类型检查、不生成产物,比 build 快,改完代码想快速验证时最常用cargo build --release:带优化编译,产物在target/release/,发布时用
debug 模式编译快但运行慢;release 模式编译慢但运行快。开发时用默认的 debug,发布用 --release。
依赖管理
Cargo 默认从 crates.io(Rust 社区的中心 registry)拉取依赖。添加依赖推荐用 cargo add:
$ cargo add serde它会自动把合适的版本写进 [dependencies] 并更新 Cargo.lock。也可以手写:
[dependencies]serde = "1.0"版本字符串是 semver 版本要求:"1.0" 表示 >=1.0.0 且 <2.0.0 的任意版本(默认 ^ 语义)。重新 cargo build 时,Cargo 会拉取依赖以及它们的传递依赖。
想升级依赖时用:
$ cargo update # 更新所有依赖$ cargo update serde # 只更新 serde另外还有 [dev-dependencies]:只在测试、示例和基准中可用的依赖,正常构建不会引入。
Cargo.toml vs Cargo.lock
这两个文件分工不同:
Cargo.toml:你写的,宽泛声明依赖(如serde = "1.0")。Cargo.lock:Cargo 维护的,记录所有依赖的精确版本和来源,不要手动改。
为什么需要 lock?serde = "1.0" 今天可能解析到 1.0.x,明天解析到另一个版本,别人 clone 后构建结果就不同了。Cargo.lock 把解析结果固定下来,这正是第一节说的”可重复构建”。
所以把 Cargo.lock 提交进版本控制(比如 git)。想升级时运行 cargo update,Cargo 会重新解析并写回 lock。
(库项目发布到 crates.io 时 Cargo.lock 不会随包发布,由使用方自己解析;但开发仓库中提交它依然是推荐做法。)
Package Layout:目录约定
Cargo 用一套约定安排文件位置,让新人看到一个项目就能快速上手。一个标准 Cargo 项目长这样:
.├── Cargo.toml├── Cargo.lock├── src/│ ├── main.rs # 默认可执行程序入口│ ├── lib.rs # 默认库文件入口│ └── bin/ # 其他可执行程序├── benches/ # 基准测试├── examples/ # 示例└── tests/ # 集成测试几个要点:
Cargo.toml和Cargo.lock放在项目根目录(package root)。- 源代码放在
src/目录。 - 默认库文件是
src/lib.rs,默认可执行文件是src/main.rs,其他可执行程序放src/bin/。 - 基准测试放
benches/,示例放examples/,集成测试放tests/。
如果某个目标(可执行程序、示例、基准或集成测试)由多个文件组成,就在对应目录下建一个子目录,里面放 main.rs 和其他模块,目标的名字就是子目录名:
src/bin/my-tool/├── main.rs└── some_module.rs命名也有约定:目标用 kebab-case(如 my-tool.rs),模块用 snake_case(如 some_module.rs)。
这些约定正是 Cargo 四件事里的第四件——Cargo 会自动发现所有目标,不需要你写配置文件告诉它每个文件在哪。 记住一个目录结构,所有 Cargo 项目就都认识了。
测试
测试函数用 #[test] 标注,cargo test 运行它们:
#[cfg(test)]mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); }}Cargo 在两类地方找测试:
src/里的单元测试(如上面的#[cfg(test)]模块,随 crate 一起编译)tests/目录里的集成测试(把 crate 当外部库使用,需要use导入)
cargo test foo 只运行名字里含 foo 的测试。另外 cargo test 还会编译 examples/ 里的示例,并运行文档注释中的代码示例(doctest)。
Workspace:把多个包放在一起管理
前面说的都是一个项目一个包。当你有多个彼此相关的包——比如一个库加上一个命令行工具——分开管理会很麻烦:每个包都有自己的 Cargo.lock 和 target/,依赖版本还得手动对齐。
Workspace(工作区)就是一组放在一起管理的包,成员(workspace members)之间共享:
- 同一个
Cargo.lock和输出目录target/,都在 workspace 根目录 - 一次对全体成员执行命令,如
cargo build --workspace - 共享元数据:
workspace.package、workspace.dependencies等 [patch]、[replace]、[profile]只在根Cargo.toml中生效
创建一个 workspace
在 Cargo.toml 里加上 [workspace] 表即可。有两种形式:
1. 根包 workspace:给一个已有 [package] 的 Cargo.toml 加 [workspace],这个包就是根包(root package):
[workspace]
[package]name = "hello_world"version = "0.1.0"2. 虚拟 workspace:Cargo.toml 只有 [workspace] 没有 [package],适合没有”主包”、或者想把所有包放进子目录的情况:
# 根目录 Cargo.toml[workspace]members = ["hello_world"]resolver = "3"[package]name = "hello_world"version = "0.1.0"edition = "2024"注意:虚拟 workspace 没有 package.edition 可以推断,所以 resolver 必须显式设置。
成员怎么定
members列出要包含的目录,支持 glob(如crates/*)exclude排除目录- workspace 目录内的所有 path 依赖 会自动成为成员
命令作用于哪些包
在 workspace 中,cargo build 默认只操作当前目录所在的包;cargo build --workspace 操作全部成员;cargo build -p foo 指定单个包。在 workspace 根目录执行时,默认作用于 default-members(未指定时,虚拟 workspace 默认全部成员)。
共享包元数据
如果所有成员用同一套 version、authors,可以在根 Cargo.toml 定义一次,成员用 *.workspace = true 继承:
# 根目录 Cargo.toml[workspace]members = ["bar"]
[workspace.package]version = "1.2.3"authors = ["Nice Folks"][package]name = "bar"version.workspace = trueauthors.workspace = true依赖也可以用同样的方式在 workspace.dependencies 中统一管理,保证所有成员用同一个版本。
常用工具
Rust 生态里还有三个高频的 cargo 子命令:
cargo fmt:按 rustfmt 标准格式化代码cargo clippy:linter,检查常见错误和坏味道,比编译器更严格cargo doc:从文档注释生成 HTML 文档(输出到target/doc/)