关于 rust-module-layout
面向 Rust 开发者的单 crate 模块组织 Skill,指导如何安排文件、模块声明、可见性与公共重导出。涵盖现代目录布局、API 门面设计及常见反模式的重构。
主要功能
- 01将模块层级映射为源码目录
- 02让 lib.rs 和 main.rs 保持精简
- 03选择现代或旧式文件布局
- 04有原则地设置 Rust 可见性
- 05设计定向公共重导出门面
- 06重构超大文件与通配符导出
适用场景
- 01规划新 crate 的源码布局
- 02拆分大型 Rust 源文件
- 03收窄过度宽泛的 crate API
- 04更新旧式 mod.rs 布局
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ full-stack-skills/ rust-skills --skill rust-module-layout安装后在 Claude Code 中输入/rust-module-layout确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlexamples/README.mdrefactoring-anti-patterns.mdsplitting-files.mdtemplates.mdvisibility-decisions.mdexamples/golden-facade/Cargo.lockCargo.tomlexamples/golden-facade/src/ast.rserror.rslib.rsparser.rsexamples/golden-facade/src/ast/print.rsexamples/golden-facade/src/parser/grammar.rslexer.rsreferences/coming-from-java-python.mdfacade-design.mdlarge-crate-layout.mdmodernizing-mod-rs.mdmodule-resolution.mdnaming.mdREADME.mdrefactoring-flat-lib-rs.mdvisibility-and-privacy.md详细介绍
Rust 模块布局(单个 Crate 内部)
权威参考:The Rust Book 第 7 章、Rust Reference 第 7 章、Rust API Guidelines — 组织(C-HIERARCHY、C-REEXPORT)、Rust Style Guide。
此技能是 rust-workspace 的 crate 内部对应技能。前者决定 crate 边界(工作区、包、依赖方向);此技能决定 单个 crate 的 src/ 内部结构:文件树、mod 声明、可见性和公开重导出接口。
能力边界
✅ 优势
- 将逻辑模块层级转换为物理
src/目录树 - 将
lib.rs/main.rs编写成精简的索引,而非内容堆积处 - 在
foo.rs/foo/mod.rs/foo/之间做出选择(Edition 2018+ 与旧版) - 为模块采用语义明确的名称(
connection而非db,runtime而非rt) - 有意使用
pub/pub(crate)/pub(super)/pub(in path) - 通过有针对性的
pub use,在深层私有树之上设计扁平的公开门面 - 识别并重构 “扁平的 lib.rs +
pub use *::*” 反模式(从 Java/Python 移植而来的 crate 中很常见) - 将 500 行以上的文件拆分到目录中,同时不破坏调用方
- 将旧式
mod.rs布局迁移到现代的foo.rs + foo/形式
⚠️ 前置条件
- Rust 所有权和基础模块语法 — 请参阅
rust-stable技能 - Crate、包和工作区的区别 — 请参阅
rust-workspace技能
❌ 不在范围内
- Cargo.toml / 依赖 / 功能 → 使用
rust-cargo-build - 将 crate 拆分为工作区成员 → 使用
rust-workspace unsafe代码块的可见性 → 使用rust-unsafe-ffi
数据隐私
此技能不会收集、存储或传输任何用户数据。
两条不可违背的规则
如果你只记住一件事,就记住这两条。所有 Rust crate 都遵循它们:
规则 1 — 目录只有在声明后才是模块
// ❌ You created src/string/case.rs. The compiler does not see it.
// ✅ You must declare every level:
// src/lib.rs
pub mod string; // loads src/string.rs OR src/string/mod.rs
// src/string.rs (modern) OR src/string/mod.rs (legacy)
pub mod case; // loads src/string/case.rs
文件放在目录中,并不会自动对编译器可见;只有父模块声明了 mod <name>; 后,该文件才可见。对于从 Java/Python/Go 转来的开发者来说,这一点最令人意外,因为在这些语言中,文件系统就是模块系统。
规则 2 — 所有内容默认都是私有的;只有 pub 才会公开
// src/string/case.rs
pub fn to_snake_case(s: &str) -> &str { ... } // ✅ callable from outside
fn helper(s: &str) -> &str { ... } // 🔒 crate-internal to this file
| 修饰符 | 可见范围 |
|---|---|
| (无修饰符) | 仅当前模块及其子孙模块 |
pub(crate) | 整个 crate,不包括外部用户 |
pub(super) | 仅父模块 |
pub(in path) | 祖先模块 path 及其子孙模块 |
pub(self) | 与无修饰符相同(很少显式书写) |
pub | 所有人(受父模块可见性限制——隐私受父级约束) |
关键细节:在条目上使用 pub 并不保证外部可见。只有从 crate 根到该条目路径上的每个模块都同样是 pub,该条目才可访问。因此,mod string;(私有)会使 string/ 下的所有内容对外部用户不可见,即使 string::case::to_snake_case 是 pub 也不例外。
决策:foo.rs、foo/mod.rs 还是 foo.rs + foo/
Rust 2018+ 引入了第三种布局,无需使用 mod.rs。三种布局都合法;请选择一种,并在同一个 crate 中保持一致。
| 布局 | 文件布局 | 适用场景 |
|---|---|---|
| 单文件 | src/foo.rs | 模块内容可放在一个文件中(经验法则:少于 300–500 行) |
| 传统目录 | src/foo/mod.rs + src/foo/bar.rs | 适用于 2018 版之前的代码库;目标版本为 Edition 2015 时必须使用 |
| 现代目录(2018+) | src/foo.rs + src/foo/bar.rs | 新代码的默认选择——每个目录少一个文件,无需编写 mod.rs 样板代码 |
// Modern layout (default for new crates)
src/
├── lib.rs // pub mod foo;
├── foo.rs // pub mod bar; pub fn root_fn() {}
└── foo/
└── bar.rs // pub fn bar_fn() {}
// Legacy layout (only if Edition 2015)
src/
├── lib.rs // pub mod foo;
└── foo/
├── mod.rs // pub mod bar;
└── bar.rs
可以混用,但不建议。 如果一个子树使用 foo/mod.rs,另一个使用 bar.rs + bar/,读者就必须逐个检查目录。每个 crate 请选择一种(验证脚本会强制检查一致性)。
标准布局模板
模板 A — 小型 crate(< ~1000 LOC,单一领域)
src/
├── lib.rs // 5–30 lines: mod decls + pub use facade
├── error.rs // crate error type
├── parser.rs // one file per major concern
├── ast.rs
└── codegen.rs
// src/lib.rs
//! One-line crate docs.
mod error;
mod parser;
mod ast;
mod codegen;
pub use error::Error;
pub use parser::parse; // targeted re-export — only the most-used items
pub use ast::{Expr, Stmt};
模板 B — 中型 crate(~1k–10k LOC,多个子领域)
src/
├── lib.rs // index + facade
├── error.rs
├── parser/
│ ├── mod.rs // (or parser.rs in modern layout) — declares submodules
│ ├── lexer.rs
│ ├── grammar.rs
│ └── error.rs // parser-specific errors, re-exported upward
├── ast/
│ ├── mod.rs
│ ├── expr.rs
│ ├── stmt.rs
│ └── visit.rs
└── codegen/
├── mod.rs
├── llvm.rs
└── cranelift.rs
// src/lib.rs
mod error;
mod parser;
mod ast;
mod codegen;
pub use error::Error;
pub use parser::Parser; // types users construct
pub use ast::{Expr, Stmt, Module}; // data types users manipulate
// Note: codegen stays internal — users don't construct it directly
模板 C — 大型 crate(10k+ LOC,深层嵌套)
与模板 B 相同,但:
- 每个顶层目录最多可嵌套 3–4 层(
parser/grammar/expr/primary.rs) lib.rs保持精简(最多约 50 行)——仅包含mod和经过筛选的pub use- 子目录中的
mod.rs文件充当子门面:向上一级重新导出最有用的条目,同时将实现细节保持为私有 - 请参阅
references/large-crate-layout.md,其中提供了一个包含 50 个文件的完整示例
公共门面模式(C-REEXPORT)
Rust API 指南(C-REEXPORT) 建议:crate 根重新导出最常用的类型,让用户编写 use my_crate::Thing,而不是 use my_crate::deep::path::Thing。
// src/lib.rs — GOOD
mod connection;
mod pool;
mod query;
mod error;
// Targeted re-exports — only the items users actually need
pub use connection::Connection;
pub use pool::Pool;
pub use query::{Query, QueryBuilder};
pub use error::Error;
// The internal tree (connection/, pool/, query/) stays private.
// Users cannot reach my_crate::connection::tcp::TcpStream even if they try.
定向重新导出与 glob 重新导出
| 形式 | 适用场景 | 不适用场景 |
|---|---|---|
pub use foo::{Bar, Baz}; | ✅ 默认选择——明确、对 IDE 友好,并要求作者逐项作出决定 | — |
pub use foo::*; | 很少使用——仅当 foo 是一个叶子模块,其中所有条目名称都稳定且都适合放在根命名空间时 | ❌ 大多数情况——会将数十个无关符号塞入根命名空间、隐藏来源,并破坏 IDE 自动补全 |
每个 foo 都使用 pub use foo::*; | 绝不使用 | ❌ 这就是扁平化 lib.rs 反模式(见下一节) |
glob 重新导出很诱人,因为它“开箱即用”——输入名称即可解析。但它确实有代价:
- 来源丢失:
my_crate::Pool——Pool实际位于哪里?pool::?connection::pool::?connection::pool::v2::?读者和 IDE 无从得知。 - 符号冲突悄然发生:来自不同子模块的两个
pub use *::*会冲突;后者覆盖前者。 - 重构不透明:将
Pool从pool::移至pool::v2::不会体现在差异中,因为重新导出使用了 glob。 - IDE 补全变成噪声:输入
my_crate::会显示数百个符号,而不是精心筛选的十几个。
反模式(以及如何重构)
这些模式常见于由 Java/Python 开发者编写的 crate 中,因为这些语言的惯例有所不同。所有示例都经过匿名化,但均取材于真实的公开 crate。
| # | 反模式 | 症状 | 修复方法 |
|---|---|---|---|
| 1 | 扁平的 lib.rs + 到处使用 glob 重新导出 | 每个模块都重复出现 pub mod foo; pub use foo::*; | 将 pub use foo::*; 替换为有针对性的 pub use foo::{A, B};;将实现模块设为私有(mod foo;) |
| 2 | src/ 根目录中的巨型文件 | 一个文件超过 500 行,混杂多个关注点 | 拆分为目录,每个关注点一个文件(参见 examples/splitting-files.md) |
| 3 | 含义模糊或过于简略的模块名 | db、io、rt、ext、util、common、core、types | 改为语义明确的名称:connection、buffer、runtime(参见 references/naming.md) |
| 4 | 隐私泄露(到处都是 pub) | pub struct Pool { pub inner: Vec<_>, pub config: _, } | 默认设为私有;仅在稳定后扩大可见范围(参见 references/visibility-and-privacy.md) |
| 5 | 过深的公开树 | my_crate::connection::tcp::stream::TcpStream | 保持树结构为私有;公开扁平门面(mod connection; pub use connection::TcpStream;) |
| 6 | Edition 2018+ crate 中使用 mod.rs | 同一个 crate 中混用 foo/mod.rs 和 bar.rs + bar/ 布局 | 每个 crate 选择一种;优先使用现代的 foo.rs + foo/ 布局(参见 references/modernizing-mod-rs.md) |
| 7 | #[macro_use] mod foo; | 2018 版之前的旧式宏导入会将所有宏泄露到整个 crate | 使用 #[macro_export] + 显式 pub use(参见 examples/refactoring-anti-patterns.md 重构 5) |
如需了解每种反模式的完整分步重构,请参见:
examples/refactoring-anti-patterns.md— 七个包含重构前后代码的完整演示references/refactoring-flat-lib-rs.md— 将索引式 + globlib.rs完整迁移为精选门面
拆分不断增长的文件
当文件超过约 500 行或开始混杂多个关注点时,将其拆分为目录。
机械式流程(调用方的行为零变化):
- 创建
src/foo/目录 - 对原始
foo.rs中的每个关注点,创建src/foo/<concern>.rs并移动相应条目 - 将
src/foo.rs替换为pub mod <concern>;声明和有针对性的重新导出,以保留旧 API - 运行
cargo check— 编译应成功,且调用方无需做任何改动
如需查看将一个 600 行文件拆分为目录的完整示例,请参见 examples/splitting-files.md。
拆分为目录还是工作区
- 拆分为目录(位于同一个 crate 中):子域共享类型、总是一起使用或紧密耦合。只有一个版本,只需发布一次。
- 拆分为工作区(拆为独立的 crate):子域可独立使用、有不同的稳定性演进轨迹或有不同的依赖范围。
请参见 rust-workspace,了解工作区层面的决策。
可见性速查表
// Public — anyone with a path to this item can use it
pub fn f() {}
// Crate-visible — usable anywhere inside this crate, not by external users
pub(crate) fn g() {}
// Parent-visible — usable in the parent module and its descendants
pub(super) fn h() {}
// Restricted to an ancestor path and its descendants
pub(in crate::foo::bar) fn i() {}
// Module-private (default) — usable only inside this module
fn j() {}
// `pub` with a private parent is effectively `pub(crate)` from the outside.
// Privacy is **parent-bound**: external reachability requires every module
// on the path from the crate root to be `pub`.
mod internal {
pub struct Hidden; // pub, but `internal` is private → unreachable externally
}
工作流
- 识别逻辑领域 — 列出 crate 涉及的关注点(解析、AST、代码生成、运行时)。每个关注点都对应一个顶层模块。
- 确定顶层可见性 — 对每个模块,判断它属于公共 API(用户会构造其中的类型)还是实现细节。将实现模块标记为
mod(私有)。 - 设计门面 — 编写
lib.rs,把它当作用户唯一会阅读的文件。列出用户需要的所有pub use。如果列表超过约 30 项,说明这个 crate 承担了太多职责——考虑拆分工作区。 - 规划目录树 — 对每个顶层模块,决定使用单个文件(模板 A)还是目录(模板 B/C)。新代码采用现代的
foo.rs + foo/布局。 - 使用语义化模块名 — 使用完整单词,不要缩写。模块名应让读者无需打开文件就能知道其中的内容。
- 尽量收紧可见性 — 一开始将所有内容设为私有。只有调用方确实需要时,才逐步放宽为
pub(crate)、pub(super),最后才是pub。 - 验证 — 运行
cargo check,然后运行cargo doc --no-deps --open。为 crate 根目录生成的index.html就是你的公共 API。如果其中列出了 100 多项,说明你的门面暴露了过多内容。
注意事项
- 可见性受父级限制。 私有
mod中的pub项无法从外部访问。许多“为什么外部用户看不到我的类型?”的问题都源于此。 - 同一个
foo不能同时存在foo.rs和foo/mod.rs— 否则编译器会报错。二选一。 #[path]会打破文件系统惯例,应仅用于生成的代码或特殊布局。如果使用,请说明原因。- 在 crate 根目录使用
pub use foo::*看似方便,但会让重构差异难以阅读,也会让 IDE 补全变得杂乱。优先使用有针对性的pub use foo::{A, B};。 - 对于大多数 crate,Edition 2018 及更高版本中不需要
extern crate foo;;只需使用use foo as bar;。仅在需要#[macro_use](很少见)或导入时重命名的 crate 中保留extern crate。 pub use crate::foo::Bar;、pub use self::foo::Bar;和pub use foo::Bar;都合法。对于当前 crate 内的绝对路径,crate::最易读;相对路径使用self::;不带前缀的foo::Bar仅当foo是外部 crate 或位于use作用域中时才可用。- 重命名模块属于破坏性变更,前提是该模块为
pub。升级主版本,或提供弃用别名:pub mod old_name { pub use crate::new_name::*; }。 mod.rs并未弃用。 Edition 2015 要求使用它,Edition 2018 及更高版本也仍支持。新代码优先采用现代的foo.rs + foo/布局,但mod.rs并没有错。
按需资源
- 带有可复制粘贴代码骨架的布局模板
- 扁平
lib.rs重构演练 - 大型 crate 布局(含 50 个文件的完整示例)
- 将
foo/mod.rs迁移为foo.rs + foo/ - 从 Java/Python 转向 Rust 模块思维
- 公共门面设计(C-REEXPORT)
- 命名约定