2. 搭建第一个Rust工程

上一节我们完成了Rust工具链的安装与验证,现在可以真正动手创建一个工程。Rust的工程化能力几乎全部由Cargo承载:它既是包管理器,也是构建系统、测试运行器和文档生成器。理解Cargo的工作方式,是后续所有实战内容的基础。

从 cargo new 开始:一个二进制项目

在任意空目录下执行:

cargo new hello_rust
cd hello_rust
cargo run

第一条命令会创建一个名为 hello_rust 的目录,并自动生成 Cargo.toml、src/main.rs 以及一个 .gitignore。默认情况下 cargo new 创建的是二进制(bin)项目,入口是 src/main.rs。如果要做库,使用 cargo new --lib my_lib,入口会变成 src/lib.rs。

生成的 Cargo.toml 内容大致如下:

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"

[dependencies]

其中 [package] 描述当前包本身,name 是包名,version 遵循语义化版本,edition 指定语言版本(2021 是当前主流选择)。[dependencies] 段用于声明外部依赖,目前为空。

项目结构:约定优于配置

Cargo 采用约定优于配置的目录布局,标准结构如下:

  • Cargo.toml:包清单,声明元数据与依赖。
  • Cargo.lock:锁定依赖的精确版本,保证可复现构建,通常提交到版本库(库项目可例外)。
  • src/:源码目录。main.rs 是二进制入口,lib.rs 是库入口。
  • src/bin/:存放额外的二进制目标,每个文件都是一个独立可执行程序。
  • tests/:集成测试目录,每个文件作为独立 crate 编译。
  • benches/:基准测试目录。
  • examples/:示例代码,可用 cargo run --example 名称 运行。
  • target/:构建产物目录,体积大,应加入 .gitignore。

模块系统与文件结构直接对应:src/main.rs 或 src/lib.rs 是 crate 根,通过 mod 声明子模块,子模块可以写在同文件内,也可以放在同名目录下的 mod.rs 或与模块同名的 .rs 文件中。例如声明 mod network; 后,编译器会查找 src/network.rs 或 src/network/mod.rs。

依赖管理:从 crates.io 引入外部库

添加依赖最直接的方式是编辑 Cargo.toml,也可以使用 cargo add(需较新版本 Cargo):

cargo add rand
cargo add serde --features derive
cargo add tokio --features full

执行后 [dependencies] 会变成类似:

[dependencies]
rand = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

版本号遵循语义化版本规则:"0.8" 等价于 ^0.8,表示允许 0.8.x 的补丁更新,但不跨到 0.9。若需要精确锁定,可写 "=0.8.5"。首次构建时 Cargo 会解析依赖并写入 Cargo.lock,之后构建都以此锁定版本为准,直到显式执行 cargo update。

依赖还可以来自 Git 仓库或本地路径,适合内部库或尚未发布的组件:

[dependencies]
my_utils = { path = "../my_utils" }
some_lib = { git = "https://github.com/example/some_lib", branch = "main" }

工作空间:多 crate 协同

当项目变大,通常需要拆分为多个 crate,例如一个库加多个二进制,或前后端共享的公共库。Cargo 工作空间(workspace)让多个包共享同一个 target/ 目录和 Cargo.lock,避免重复编译。

在根目录创建 Cargo.toml:

[workspace]
members = ["core", "cli", "server"]
resolver = "2"

然后分别在各子目录用 cargo new --lib core、cargo new cli 等创建成员。成员之间通过路径依赖互相引用:

[dependencies]
core = { path = "../core" }

在工作空间根目录执行 cargo build 会构建所有成员;cargo build -p cli 只构建指定包;cargo test --workspace 运行全部测试。工作空间还支持在根 Cargo.toml 中定义 [workspace.dependencies],让各成员统一版本,减少不一致。

构建与运行:常用命令与产物

核心命令及其作用:

命令作用
cargo build调试模式构建,产物在 target/debug/
cargo build --release优化构建,产物在 target/release/
cargo run构建并运行二进制
cargo check只做类型检查,不生成可执行文件,速度最快
cargo test编译并运行单元测试与集成测试
cargo doc --open生成文档并在浏览器打开
cargo clean删除 target/ 目录

日常开发中,cargo check 是迭代最快的手段,因为它跳过了代码生成和链接阶段。只有在需要实际运行或发布时才使用 build 或 run。

调试构建默认开启溢出检查、调试符号,且不做优化,适合开发;发布构建会开启优化、关闭调试断言,适合交付。两者产物目录不同,互不干扰。

依赖解析与构建流程

下面这张图展示了从编辑 Cargo.toml 到最终运行可执行文件的完整链路,包括依赖解析、锁定、编译与产物落地的位置关系。

Cargo.toml 包元数据 + 依赖声明 src/ 源码 main.rs / lib.rs / 模块 依赖解析 crates.io / git / path Cargo.lock 锁定精确版本,可复现 rustc 编译 类型检查 → 代码生成 → 链接 target/debug/ 调试产物 target/release/ 优化产物 cargo run / 直接执行可执行文件 程序启动,加载依赖动态库(如有) 输入 解析与锁定 编译 运行

理解这条链路后,很多现象就变得可解释:为什么第一次构建慢、之后快(依赖已缓存);为什么 cargo check 比 build 快(跳过代码生成);为什么 Cargo.lock 要提交(保证团队与 CI 使用相同依赖版本)。

一个可复现的最小工作空间示例

下面给出一个可直接照做的多 crate 工作空间骨架,包含一个库和一个二进制:

# 根目录 Cargo.toml
[workspace]
members = ["core", "cli"]
resolver = "2"

# core/Cargo.toml
[package]
name = "core"
version = "0.1.0"
edition = "2021"

# cli/Cargo.toml
[package]
name = "cli"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }

在 core/src/lib.rs 中写一个简单函数:

pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

在 cli/src/main.rs 中调用它:

fn main() {
    let msg = core::greet("Rust");
    println!("{}", msg);
}

在根目录执行 cargo run -p cli,终端会输出 Hello, Rust!。这说明工作空间成员之间的路径依赖、编译顺序和运行入口都已正确配置。

常见问题:如果 cargo run 报错“could not find Cargo.toml”,说明当前目录不在任何包或工作空间内;如果报“no bin target”,说明当前包是库而非二进制,需要用 cargo run -p 包名 指定,或改用 cargo test 验证库逻辑。