Cargo 教程

2026年9月6日
7 min read
cargo_tutorial

为什么需要 Cargo

在 Rust 中,一个库或可执行程序叫 crate,由编译器 rustc 编译。学 Rust 的第一个程序 “Hello, world!” 可以直接用 rustc 编译:

fn main() {
println!("Hello, world!");
}
Terminal window
$ rustc hello.rs
$ ./hello
Hello, world!

但直接使用 rustc 有两个问题:

  1. 命令不统一。 你必须显式指定文件名 hello.rs,编译不同程序需要不同命令行,加上编译参数和外部依赖后命令会更复杂。
  2. 依赖难管理。 正经项目几乎都会用外部库,外部库又传递依赖别的库。手工获取所有依赖的正确版本并保持更新,既困难又容易出错。

因此 Rust 在 crate 之上引入了更高层的抽象——package(包),以及配套的包管理器 Cargo

Cargo 是什么

Cargo 是 Rust 的包管理器。 它让包声明自己的依赖,并保证可重复的构建:无论在什么机器、什么时间构建,结果都一致可预期。

为此 Cargo 做四件事:

  1. 引入两个元数据文件:Cargo.toml(包的元信息与依赖声明)和 Cargo.lock(锁定依赖的精确版本)。
  2. 自动从 registry(如 crates.io)拉取并构建依赖。
  3. 以正确的参数调用 rustc,把依赖安排进构建。
  4. 引入约定,让使用 Rust 包更简单。

其中约定最有价值:Cargo 把命令标准化了。 无论项目叫什么、是库还是可执行程序,构建命令都是同一条:

Terminal window
$ cargo build

所以并不夸张地说:学会构建一个 Cargo 项目,就学会了构建所有 Cargo 项目。

小结

Cargo 把 Rust 项目从”手工编译 + 手工管理依赖”变成”声明依赖 + 统一命令 + 可重复构建”的工程化状态。接下来我们动手:安装 Cargo,创建第一个项目。

安装 Cargo

Cargo 随 Rust 一起安装,不需要单独装。官方推荐用 rustup 安装 Rust:

Terminal window
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

装好后运行 rustc --versioncargo --version,能看到版本号就说明安装成功。

rustup 负责管理 Rust 工具链和版本,以后升级就运行 rustup update

创建第一个项目

cargo new 创建:

Terminal window
$ cargo new hello_world --bin
$ cd hello_world

--bin 表示创建二进制程序(可执行文件);创建库用 --lib,不传时默认 --bin。另外 cargo new 默认会初始化一个 git 仓库,不想要可以加 --vcs none。如果代码已经在一个目录里,用 cargo init 初始化(它不会新建目录)。

生成的结构:

.
├── Cargo.toml
└── src
└── main.rs

Cargo.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 程序。

构建与运行

Terminal window
$ cargo build
$ cargo run
  • cargo build:编译,产物在 target/debug/
  • cargo run:编译并运行,一步到位
  • cargo check:只做类型检查、不生成产物,比 build 快,改完代码想快速验证时最常用
  • cargo build --release:带优化编译,产物在 target/release/,发布时用

debug 模式编译快但运行慢;release 模式编译慢但运行快。开发时用默认的 debug,发布用 --release

依赖管理

Cargo 默认从 crates.io(Rust 社区的中心 registry)拉取依赖。添加依赖推荐用 cargo add

Terminal window
$ cargo add serde

它会自动把合适的版本写进 [dependencies] 并更新 Cargo.lock。也可以手写:

[dependencies]
serde = "1.0"

版本字符串是 semver 版本要求:"1.0" 表示 >=1.0.0<2.0.0 的任意版本(默认 ^ 语义)。重新 cargo build 时,Cargo 会拉取依赖以及它们的传递依赖。

想升级依赖时用:

Terminal window
$ cargo update # 更新所有依赖
$ cargo update serde # 只更新 serde

另外还有 [dev-dependencies]:只在测试、示例和基准中可用的依赖,正常构建不会引入。

Cargo.toml vs Cargo.lock

这两个文件分工不同:

  • Cargo.toml你写的,宽泛声明依赖(如 serde = "1.0")。
  • Cargo.lockCargo 维护的,记录所有依赖的精确版本和来源,不要手动改。

为什么需要 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.tomlCargo.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.locktarget/,依赖版本还得手动对齐。

Workspace(工作区)就是一组放在一起管理的包,成员(workspace members)之间共享:

  • 同一个 Cargo.lock 和输出目录 target/,都在 workspace 根目录
  • 一次对全体成员执行命令,如 cargo build --workspace
  • 共享元数据:workspace.packageworkspace.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. 虚拟 workspaceCargo.toml 只有 [workspace] 没有 [package],适合没有”主包”、或者想把所有包放进子目录的情况:

# 根目录 Cargo.toml
[workspace]
members = ["hello_world"]
resolver = "3"
hello_world/Cargo.toml
[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 默认全部成员)。

共享包元数据

如果所有成员用同一套 versionauthors,可以在根 Cargo.toml 定义一次,成员用 *.workspace = true 继承:

# 根目录 Cargo.toml
[workspace]
members = ["bar"]
[workspace.package]
version = "1.2.3"
authors = ["Nice Folks"]
bar/Cargo.toml
[package]
name = "bar"
version.workspace = true
authors.workspace = true

依赖也可以用同样的方式在 workspace.dependencies 中统一管理,保证所有成员用同一个版本。

常用工具

Rust 生态里还有三个高频的 cargo 子命令:

  • cargo fmt:按 rustfmt 标准格式化代码
  • cargo clippy:linter,检查常见错误和坏味道,比编译器更严格
  • cargo doc:从文档注释生成 HTML 文档(输出到 target/doc/