1. Cargo 创建与模块化组织 Rust 项目
为什么需要“二进制 + 库”混合项目
当项目既有可执行入口,又希望把核心逻辑做成可复用、可测试的库时,Cargo 的标准做法是:同一个包内同时提供 src/main.rs 与 src/lib.rs。二进制目标负责启动与参数解析,库目标承载业务模块,测试代码直接针对库模块编写,避免把逻辑塞进 main 后难以单测。
本案例创建一个名为 taskhub 的混合项目:库中按职责拆分为 models、store、service 三个模块,二进制入口只做调用与输出。通过 mod 声明模块、pub use 收敛对外接口,最后用 cargo build 与 cargo test 验证编译与测试均通过。
创建项目与目录结构
在终端执行以下命令,Cargo 会自动生成二进制与库两个目标文件:
cargo new taskhub
cd taskhub
随后手动补充模块文件,最终目录结构如下(src/lib.rs 与 src/main.rs 由 Cargo 生成,其余为新增):
taskhub/
├── Cargo.toml
└── src/
├── main.rs # 二进制入口
├── lib.rs # 库根,声明并导出模块
├── models.rs # 数据结构
├── store.rs # 存储与查询
└── service.rs # 业务逻辑
模块划分与可见性设计
模块拆分的核心是职责边界与可见性控制:models 只定义数据结构;store 持有数据并提供增删查;service 组合 store 完成业务动作。库根 lib.rs 用 pub use 把常用类型与函数提升到 crate 顶层,调用方无需关心内部层级。
关键代码实现
src/models.rs — 定义任务数据结构,字段对外可见:
#[derive(Debug, Clone, PartialEq)]
pub struct Task {
pub id: u32,
pub title: String,
pub done: bool,
}
impl Task {
pub fn new(id: u32, title: &str) -> Self {
Task { id, title: title.to_string(), done: false }
}
}
src/store.rs — 内存存储,提供添加与查询:
use crate::models::Task;
#[derive(Default)]
pub struct TaskStore {
tasks: Vec<Task>,
}
impl TaskStore {
pub fn add(&mut self, task: Task) {
self.tasks.push(task);
}
pub fn all(&self) -> &[Task] {
&self.tasks
}
pub fn count(&self) -> usize {
self.tasks.len()
}
}
src/service.rs — 业务逻辑,组合 store 与 models:
use crate::models::Task;
use crate::store::TaskStore;
pub fn add_task(store: &mut TaskStore, id: u32, title: &str) -> Task {
let task = Task::new(id, title);
store.add(task.clone());
task
}
pub fn pending_count(store: &TaskStore) -> usize {
store.all().iter().filter(|t| !t.done).count()
}
src/lib.rs — 声明模块并用 pub use 收敛接口:
pub mod models;
pub mod store;
pub mod service;
pub use models::Task;
pub use service::{add_task, pending_count};
pub use store::TaskStore;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_and_count() {
let mut store = TaskStore::default();
add_task(&mut store, 1, "write docs");
add_task(&mut store, 2, "review PR");
assert_eq!(store.count(), 2);
assert_eq!(pending_count(&store), 2);
}
#[test]
fn task_defaults_to_pending() {
let t = Task::new(9, "deploy");
assert!(!t.done);
assert_eq!(t.title, "deploy");
}
}
src/main.rs — 二进制入口,只调用库的公开接口:
use taskhub::{add_task, pending_count, TaskStore};
fn main() {
let mut store = TaskStore::default();
add_task(&mut store, 1, "set up cargo project");
add_task(&mut store, 2, "split modules");
println!("total tasks: {}", store.count());
println!("pending: {}", pending_count(&store));
for t in store.all() {
println!(" [{}] {} (done={})", t.id, t.title, t.done);
}
}
编译与测试验证
在项目根目录依次执行:
cargo build
cargo test
cargo run
预期结果:cargo build 编译通过,生成 target/debug/taskhub;cargo test 运行 2 个单元测试并全部通过(输出 test result: ok. 2 passed);cargo run 打印总任务数与待办数,并逐条列出任务。
cargo build 报 unresolved import,优先检查 lib.rs 中是否已用 pub mod 声明对应模块;若 main.rs 无法访问类型,检查 lib.rs 是否用 pub use 将其重导出到 crate 顶层。模块文件缺失时,Cargo 会提示 file not found for module,按提示补齐 src/ 下的文件即可。
pub use 只改变对外暴露路径,不改变模块内部可见性。若希望某个类型仅在 crate 内使用,应使用 pub(crate) 而非 pub,避免把内部实现细节暴露给下游依赖。