程序员的大型 Rust 项目初体验:代码组织是第一道坎的真实感受
程序员的大型 Rust 项目初体验:代码组织是第一道坎的真实感受
一、第一次打开仓库时我像个无头苍蝇
入职第一天,mentor 甩给我一个 Git 地址,说"先跑起来熟悉熟悉"。我 clone 下来打开,看到这样的目录结构瞬间懵了:
log-platform/ ├── crates/ │ ├── pipeline-core/ │ ├── pipeline-parser/ │ ├── pipeline-filter/ │ ├── pipeline-output-sink/ │ ├── pipeline-cdc/ │ ├── shared-types/ │ ├── shared-utils/ │ └── api-server/ ├── examples/ ├── tests/ └── Cargo.toml8 个 crate,每个里面又有十几二十个文件。我一个自学出身的,连mod.rs和lib.rs的区别都没完全搞懂,就在这 8 万行代码里硬着头皮看。
当时我做的第一件事不是读代码,而是在笔记本上手画了一张依赖图:
画完这张图之后,项目的骨架才在我脑子里立起来。的同学如果第一次看大型 Rust 项目,强烈建议做这一步——不看具体代码实现,先看依赖方向,搞清楚"谁依赖谁"。
二、mod.rs 和我较劲了两周
对我来说,最折磨的不是 Rust 的 borrow checker,而是mod模块系统。在写小型项目时,一个main.rs+ 两三个mod xxx;声明就够了,从来不觉得这是问题。但到了大型项目里,模块层级一深,pub、pub(crate)、pub(super)加上use路径,让我经常在一个文件里改了半天,编译报 30 个错都是因为可见性没写对。
// ============================================================ // crate::pipeline::mod.rs — 我曾经最怕的文件 // ============================================================ // 声明子模块,Rust 会在同名文件/目录中查找 pub mod pipeline; // 核心 pipeline trait pub mod parser; // 日志解析器 pub mod filter; // 日志过滤器 pub mod output; // 输出目标(数据库、文件等) // 重新导出常用类型,让外部调用方不用关心内部结构 // 这是 "Facade Pattern" 的简单版本 pub use pipeline::Pipeline; pub use parser::LogEntry; pub use filter::FilterRule; pub use output::OutputSink;一开始我特别不理解"为什么要把已经 pub 的东西再pub use一次",觉得是冗余代码。后来 mentor 给我看了一个比较:
如果不用pub use重导出,外部 crate 引用时要写:
use pipeline_core::pipeline::parser::LogEntry; // 太深了!有了pub use重导出后:
use pipeline_core::LogEntry; // 简洁清晰我才明白这本质上和 API 设计一样:内部怎么组织是你的事,但暴露给外部的接口要尽量扁平。
三、shared-types 让我理解了"核心模型"的重要性
这个项目里最"平淡"却最重要的 crate 是shared-types。它只有类型定义,没有任何业务逻辑:
// ============================================================ // shared-types/src/lib.rs // ============================================================ use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; /// 一条从日志文件解析出的结构化日志条目 /// 这是整个数据管道的"语言"——所有 crate 都用这个结构通信 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { /// 日志产生的时间戳 pub timestamp: DateTime<Utc>, /// 日志级别(INFO、WARN、ERROR 等) pub level: LogLevel, /// 产生日志的服务名称 pub service: String, /// 日志来源主机 pub host: String, /// 原始日志文本 pub message: String, /// 解析出的结构化字段(不同日志格式有不同的 key-value) pub fields: HashMap<String, String>, } /// 日志级别枚举 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum LogLevel { Debug, Info, Warn, Error, Fatal, }我问 mentor 为什么把类型定义单独抽出来。他说了一段我记到现在的话:"当 8 个 crate 都用同一个 struct 时,你不希望谁改了字段大小导致序列化不一致。shared-types 就像团队的宪法——要改一起改,要兼容一起兼容。"
这跟我以前一个人写代码时完全不一样。一个人写的时候,改个字段名随手改了就行;多人协作时,一个字段的修改可能让 3 个 crate 的 CI 都挂掉。
四、依赖方向是我学到的最重要的一课
这个项目有一个铁律:依赖只能单向流动。shared-types不被任何人依赖?谁都不能依赖shared-types以外的"同级" crate。
具体来说:
shared-types→ 零依赖(除了第三方库)pipeline-core→ 只依赖shared-typespipeline-parser→ 只依赖pipeline-core和shared-types- 绝对不允许
pipeline-parser依赖pipeline-filter
一旦出现循环依赖,Cargo workspace 虽然不会报编译错误(因为 Rust 不允许 crate 间循环依赖),但会导致你不得不把代码揉在一起,破坏分层。
后来 mentor 让我做了一次小的重构任务:把pipeline-filter里一段想依赖pipeline-parser的代码搬到了pipeline-core。过程极其痛苦——我改了 12 个文件,跑了 3 遍全量测试。但做完之后,依赖图变干净了,那个"看起来方便"的跨层调用去掉了。
这就是自学转码最难学的东西:不是语法,不是算法,是架构的"品味"。
五、总结
从单文件脚本到 8 万行的多人项目,我最大的感受是:Rust 教会我的不只是怎么写安全的代码,更是怎么组织代码让团队能协作。
三个对同学最实用的建议:
- 接手项目先画依赖图。不要一上来就看代码细节,搞清楚 crate 之间的依赖方向比什么都重要。
- 重视 shared-types。多人协作时,公共类型定义就是你们的"接口合约",放一起统一管理。
- 依赖方向是单向的。从底层的类型定义 → 核心逻辑 → 功能模块 → 对外接口,不允许往回依赖。这个原则在 Rust 里被编译器强制,但也需要你在设计时就做好规划。
下一篇文章我会写 Tokio runtime 的调优经历——那是另一个让我脱了层皮的故事。