第一个程序与 cargo 项目结构

装好工具链之后,下一步是搞懂一个 cargo 项目里每个文件都是干嘛的。这篇用最短路径带你从零建一个项目、改代码、跑测试,并把 Cargo.toml、src/main.rs、src/lib.rs、target/、Cargo.lock 这些名字一次性对上号。读完你打开任何 Rust 仓库都能立刻知道该往哪里写代码、产物从哪里取、依赖从哪里来。

一、新建并运行第一个项目

在终端里执行 cargo new greeting,cargo 会生成一个最小可运行的项目。切进目录后 cargo run,你会看到屏幕上打印出 Hello, world!。现在我们把它改得稍微像样一点,接收一个名字参数并问候:

// greeting/src/main.rs
fn main() {
    let name = "Rust 学习者";
    println!("你好,{}!", name);
    println!("今天是 Rust 之旅的第一站。");
}

println! 名字里的感叹号说明它是一个宏而不是函数。宏在编译期展开,这里用于把参数按 {} 占位符格式化进字符串。写成宏而不是变参函数,是因为 Rust 的函数不支持不定数量的参数,编译期宏既能保证类型检查又能兼顾语法糖。除了 {},还有 {:?} 调试输出与 {:>8} 之类的对齐控制,日常调试用 {:?} 最多。常用占位符整理如下。

占位符 含义 示例输入 输出形态
{} Display 格式 整数、字符串 直接输出
{:?} Debug 格式 任意可调试类型 带结构信息
{:>8} 右对齐占 8 位 短数字 前面补空格
{:08} 补零占 8 位 数字 42 00000042
{:<8} 左对齐占 8 位 短文本 后面补空格

如果只要快速验证某个语法,不必建项目,单个文件用 rustc demo.rs && ./demo 也能跑,但只要超过一个文件或用了外部依赖,就老老实实用 cargo。println! 之外还有 eprintln! 输出到标准错误、print! 不换行输出,命令行工具里区分标准输出与标准错误很关键,管道行为完全不同。

二、目录结构与职责

cargo new 生成的目录虽然只有寥寥几个文件,但每个位置都有约定。下表把目录里每一项的职责列清楚。

路径 职责 你需要改它吗
Cargo.toml 项目元数据与依赖清单 添加依赖时改
src/main.rs 二进制入口 写可执行程序时改
src/lib.rs 库入口 写可复用逻辑时改
src/ 其他文件 模块代码 按模块拆分时加
target/ 构建产物与缓存 永远不要手动改
Cargo.lock 依赖精确版本锁 二进制项目随源码提交
tests/ 集成测试目录 写集成测试时加
examples/ 示例代码目录 写用法演示时加

核心约定是:main.rs 只放程序入口与参数解析,真正的业务逻辑放进 lib.rs,这样测试时可以不启动整个程序而只测函数。很多新手把全部代码堆在 main.rs,写测试时才发现无处下口。养成这个拆分习惯越早越好。

三、Cargo.toml 都写什么

Cargo.toml 是项目的中央配置,用 TOML 格式组织。一个典型示例如下:

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

[dependencies]
serde = { version = "1", features = ["derive"] }

[package] 段记录名字、版本、Rust 版次(edition),[dependencies] 段逐行列出外部依赖。版本号写法 version = "1" 等价于 >=1.0.0, <2.0.0,是 semver 兼容范围。features 字段按需开启依赖的可选功能,serde 的 derive 功能让你用注解自动生成序列化代码,这是省去手写实现的常见手段。

下表把最常用的几个段汇总一下,方便查阅。

段名 作用 常见键
[package] 描述当前包 name、version、edition
[dependencies] 运行时依赖 版本号、features
[dev-dependencies] 仅测试用依赖 写法同上
[profile.release] 发布优化设置 opt-level、lto

edition 的存在是为了让语言能平滑演进:代码注明 edition 后,即使编译器升级,旧代码也能按旧版次语义编译。对初学者来说,用工具链默认的最新版次即可,不必关心历史差异。

依赖来源不限于 crates.io:git 仓库(写 git = "...")、本地路径(写 path = "../common")都支持,开发期把本地公共库以路径依赖接进来是很常见的做法,等稳定后再发布到仓库。版本范围里还能用 版本 = "0.1" 这类写法,含义同样是 semver 区间,具体语义以官方 cargo 手册为准,记不住没关系,cargo add 包名 会自动生成推荐写法。

四、库与二进制并存

一个项目可以同时是库和二进制:src/lib.rs 提供公共 API,src/main.rs 做薄薄的命令行封装。main 里通过包名引用库里的函数:

// src/lib.rs
pub fn greet(name: &str) -> String {
    format!("你好,{}!", name)
}

// src/main.rs
use greeting::greet;

fn main() {
    println!("{}", greet("Rust 学习者"));
}

这种结构在测试时尤其划算:库函数可以用单元测试直接覆盖,不必走完整的命令行启动路径。等你把项目模块拆细之后,自然会演化出多个子目录与多个二进制,届时 Cargo.toml 里的 [[bin]] 与 [lib] 段可以显式声明额外的可执行目标。一个常见模式是 CLI 工具套件:一个 workspace 下挂多个小二进制,共享同一个库,team 里的每个成员只维护自己那把瑞士军刀。

workspace 的概念值得提前知道:当项目膨胀到多个子 crate 时,[workspace] 段把它们归拢进同一份 Cargo.lock 与同一个 target 目录,既保证版本一致又避免重复编译。入门阶段单 crate 就够用,知道这条路存在即可。

五、从需求到产物的完整流程

把前面的零碎串起来,日常开发的节奏大致是五步循环:改代码、cargo check 快速验证类型、cargo test 跑测试、cargo clippy 挖潜在问题、cargo build --release 出最终产物。前四步在开发期高频使用,最后一步只在需要分发或测性能时执行。首次全量编译比较慢,因为要把依赖全部编译一遍,之后增量编译只处理改动的部分。养成先 check 再 run 的习惯能显著缩短反馈周期,尤其在大型项目里。

一个常见的认知误区是把 target 目录当成版本管理的一部分——它体积可以膨胀到几个 GB,正确做法是早早把它加进忽略列表。CI 环境里则可以用缓存目录配合 sccache 之类的工具加速重复构建。

若你希望进一步把这套流程做成自动化的开发流,Claude Code 使用指南 中有把测试、检查、提交串成一条命令的实践。等项目长到需要图形界面时,Tauri 桌面开发指南 会展示如何复用同一个 cargo 工程结构。

延伸阅读

  • 下一节「变量、常量与遮蔽」开始进入语言本身,是写业务代码的前置知识。
  • 文本处理是命令行工具的高频需求,正则表达式精进 的示例可以直接照进本章的项目结构里练手。
  • 想观察 cargo 实际执行了哪些编译步骤,可以在任意命令后追加 --verbose 参数。