22-包、Crate 与模块
20 分钟阅读
包、Crate 与模块 (Packages, Crates and Modules)
面向 Rust 1.97.1 (stable, 2026-07)。如果你习惯了 Go 的“目录 + package 名 + 首字母大写导出”,这一篇最重要的任务就是帮你把 Rust 的三层概念拆开。
先别急着背语法。只要先分清 package、crate、module 分别在管什么,
mod、use、pub的很多报错就会自己变得顺眼。
Rust 的工程组织比 Go 更显式:Cargo.toml 描述 package,crate 是编译单元,module 是命名空间和可见性边界。文件系统只是默认映射方式,不是唯一真相。
Q1. package、crate、module 分别是什么?
Tags: hot package crate module
适用版本: Rust 1.0+
一句话答案: package 是 Cargo 管理单位,crate 是编译单位,module 是 crate 内部的命名空间与可见性边界。
解答: 一个 package 对应一份 Cargo.toml;一个 package 最多有一个 library crate,但可以有多个 binary crate;module 则是 crate 内部的树状结构,用来组织代码和控制哪些符号能穿过边界被访问。
| |
| |
第一段代码只展示 module 的概念;第二段目录树展示 package 和 crate 的落点。不要把“一个目录”自动等同于“一个 crate”。
Go 对比:
- Go 更强调目录和 package 名的对应关系。
- Rust 额外多了 crate 这层编译单元,因此同一个 package 可以同时产出库和多个二进制。
use路径看的首先是模块树和 crate 名,不只是物理目录名。
记忆点:
- package 管 Cargo 工程。
- crate 管编译产物。
- module 管命名空间和可见性。
Q2. lib.rs 和 main.rs 到底是什么关系?
Tags: hot lib.rs main.rs
适用版本: Rust 1.0+
一句话答案: 它们是两个不同 crate 的根文件;同一个 package 里可以同时存在,而且 main.rs 可以通过包名引用 lib.rs 暴露的公共 API。
解答: src/lib.rs 是 library crate root,src/main.rs 是默认 binary crate root。最常见的组织方式是:把可复用逻辑放进 lib.rs,把命令行解析、进程退出码这类入口逻辑放进 main.rs。
| |
真实项目里,这段通常会拆成 src/lib.rs 和 src/main.rs 两个文件;这里写成单文件只是为了让行为能直接编译验证。代码里的 my_app 对应真实项目中的包名;如果包名里有连字符,比如 my-app,代码里要写成 my_app。
Go 对比:
- Go 的
package main和可导入包不会共存在同一个目录里扮演两种角色。 - Rust 则允许一个 package 同时提供“库接口”和“命令行入口”,这一点对工具型项目很方便。
- 想测试和复用都更顺手时,优先把核心逻辑放进库 crate。
记忆点:
lib.rs和main.rs不是“同一文件的两种模式”,而是两个 crate。- 入口逻辑放
main.rs,业务逻辑放lib.rs。 main.rs引用库时用包名路径。
Q3. mod foo; 为什么老报 file not found for module?
Tags: hot mod E0583
适用版本: Rust 1.0+
一句话答案: 因为 mod foo; 不是“导入符号”,而是“把某个文件纳入模块树”,Cargo 会按固定文件名约定去找它。
解答: 在某个文件里写 mod foo; 后,编译器会去这个文件的同级位置寻找 foo.rs 或 foo/mod.rs。找不到时就会报 E0583。很多人是把 mod 和 use 的职责弄混了:前者建模块树,后者只是在当前作用域引入路径别名。
| |
| |
第二段不是伪代码,而是你真正会在终端里看到的核心报错信息。先补对文件位置,再看导出问题。
Go 对比:
- Go 没有
mod这一步,文件放进同一 package 目录就自动参与构建。 - Rust 更显式,因此模块树是“你声明了什么”而不是“目录里恰好有什么”。
- 一看到
E0583,先查文件路径和声明位置,不要急着改pub。
记忆点:
mod负责纳入模块树。use负责把路径带进当前作用域。E0583优先检查文件命名和层级。
Q4. use、crate::、self::、super:: 什么时候用?
Tags: common use path
适用版本: Edition 2018+
一句话答案: crate:: 从当前 crate 根开始,self:: 从当前模块开始,super:: 从父模块开始;use 只是把这些路径引进当前作用域。
解答: 这套路径系统的关键不是死记,而是先判断“我要从哪里出发找这个符号”。绝对路径更稳,尤其是库代码里;相对路径适合局部模块内部短跳转。
| |
| |
如果你在大型 crate 里频繁改模块位置,优先用 crate:: 会更不容易被局部重构影响。
Go 对比:
- Go 的 import 基本都是包级绝对路径,没有
super这种相对层级引用。 - Rust 模块树更像文件系统,但仍是语言级命名空间,不完全等于磁盘目录。
- 绝对路径心智更稳,相对路径写起来更短,二者各有场景。
记忆点:
- 稳定引用优先
crate::。 - 父子模块短路径可用
super::/self::。 use不改变可见性,只是起别名。
Q5. pub、pub(crate)、pub(super) 有什么区别?
Tags: common visibility
适用版本: Rust 1.18+
一句话答案: 它们都在放宽默认私有规则,但放开的边界不同:对所有外部可见、对当前 crate 可见、或只对父模块可见。
解答: Rust 默认一切私有,这和 Go 的“大写导出”完全不同。你需要主动决定一个符号要暴露到多远。最实用的经验是:能收窄就收窄,不要一上来全写 pub。
| |
| |
如果你把内部 helper 也随手公开,后面就很难安全重构,因为外部代码可能已经依赖上这些实现细节。
Go 对比:
- Go 只有包内私有和包外公开两层,规则靠标识符首字母控制。
- Rust 把可见范围做成了语法的一部分,因此能更细粒度地收紧 API 面。
- 大多数内部工具函数在 Rust 里适合
pub(crate),而不是直接pub。
记忆点:
- 默认私有,不是默认公开。
pub(crate)很适合 crate 内部共享实现。- 公开范围越小,未来越好重构。
Q6. 为什么写了 pub fn 还是“外部看不见”?
Tags: common pub
适用版本: Rust 1.0+
一句话答案: 因为 Rust 看的是整条路径是否都公开,不是只看最终那个函数有没有 pub;外部 crate 尤其进不了非 pub 的中间模块。
解答: 如果父模块本身是私有的,那么里面就算有 pub fn,外部 crate 也还是进不去。这个规则经常让 Go 用户困惑,因为 Go 的导出只看名字,不看路径中间层是否“通路打通”。
先看库 crate(provider)里的写法:同 crate 内,父模块可以走进私有子模块;但这对外部消费者没有任何帮助。
| |
换到另一个 crate(consumer 依赖 provider)时,非 pub 路径直接不可见:
| |
外部 crate 只能走整条都公开的路径。末端写了 pub fn 不够;private_mod 这扇门没开,外面就进不去。正确写法是只依赖公开导出:
| |
Go 对比:
- Go 只要名字大写且包被 import 到了,基本就能访问。
- Rust 则要求“路径上每一层门都打开”,而且要跨过 crate 边界。
- 一旦外部访问不到,别只盯着末端函数,先沿模块路径逐层检查
pub。
记忆点:
pub不是单点开关,要看整条路径。- 同 crate 可见不等于外部 crate 可见。
- 排查导出问题时,从外到内看路径层级。
Q7. pub use 重导出到底解决了什么问题?
Tags: common pub use
适用版本: Rust 1.0+
一句话答案: 它能把深层模块里的类型或函数提升到更稳定、更好用的公共入口,而不暴露整棵内部树结构。
解答: 这对库设计尤其重要。你可以在内部自由调整模块分层,但对外只承诺一层更短、更稳定的路径。很多成熟 crate 都会在根模块或 prelude 里做这样的重导出。
| |
| |
如果没有 pub use,使用者就必须依赖更深的内部路径,一旦你重构模块布局,外部 API 也会被迫破坏性变更。
Go 对比:
- Go 没有语言级重导出;通常是再包一层函数或重新定义类型别名来做 API 整理。
- Rust 的
pub use更直接,特别适合给库提供“短入口”。 - 设计公共 API 时,别让用户被迫记住深层内部模块路径。
记忆点:
pub use用来稳定公共入口。- 内部结构可深,对外路径最好短。
- 重导出不是多余包装,而是 API 设计工具。
Q8. 同一个 package 里放多个二进制该怎么组织?
Tags: common bin
适用版本: Rust 1.0+
一句话答案: 默认二进制放 src/main.rs,其他二进制放 src/bin/*.rs,共享逻辑优先放进 src/lib.rs。
解答: 这类布局在“一个库配多个小工具”场景里很好用。例如一个项目既有主 CLI,又有导入器、诊断工具、一次性管理脚本。共享逻辑如果写在各自的 main.rs 里,后面会很难测试和复用。
| |
| |
如果一个工具已经长成独立产品,再考虑拆成 workspace 成员 crate;在那之前,src/bin 往往足够清晰。
Go 对比:
- Go 里常见做法是
cmd/app1、cmd/app2目录下各放一个main包。 - Rust 的
src/bin很像这个思路,只是共享逻辑更自然地回到lib.rs。 - 命令越多,越要警惕把共享逻辑复制到多个入口文件里。
记忆点:
- 单 package 多命令:优先
src/bin。 - 共享逻辑放
lib.rs。 - 入口只做参数解析和调度最清爽。
Q9. 什么是 workspace,和普通 package 有什么本质区别?
Tags: common workspace
适用版本: Cargo workspace
一句话答案: workspace 让多个 package 共用一份依赖解析图和锁文件,适合把大型项目拆成多个相互协作的 crate。
解答: 当一个仓库里开始出现“核心库”“CLI”“服务端”“测试工具”这些边界时,workspace 比一个超大 package 更容易维护。它共享 Cargo.lock 和 target/,并允许统一依赖版本和 metadata。
| |
| |
workspace 不是模块系统的替代品,而是更高一层的仓库组织手段:crate 之间依然是明确依赖关系,不会因为“在同一个仓库里”就自动互相可见。
Go 对比:
- 它和
go work最像的一点是“在一个仓库里协调多个模块/包集合”。 - 但 Cargo workspace 对依赖解析、锁文件、统一命令入口的参与更深。
- 如果你只是想拆文件,用 module;如果你要拆编译边界,用 crate / workspace。
记忆点:
- module 是 crate 内部组织。
- workspace 是多 package 协作组织。
- 同仓库不等于自动可见,依赖仍要显式声明。
Q10. path、git、crates.io 依赖分别怎么选?
Tags: common dependencies
适用版本: Cargo
一句话答案: 团队本地协作优先 path,临时跟远程仓库提交可用 git,正式发布和稳定复用优先 crates.io 版本依赖。
解答: 这三种写法分别对应不同成熟度阶段。path 最适合同仓库或本地联调;git 适合“还没发版但必须引用某个提交”;真正对外发布的库,最好尽量依赖 crates.io 上已经发布的版本。
| |
公开发布到 crates.io 时,如果你的库还依赖未发布的本地 path crate,通常会直接卡住发布流程。因此内部联调用 path 没问题,但准备发布前要规划好依赖边界。
Go 对比:
- Go 依赖更倾向于模块路径加版本,直接引用本地目录的日常感没有 Cargo 这么强。
- Cargo 的
path对 monorepo 联调非常方便,但也更容易让人忘记发布边界。 git依赖不是长期解法,通常只是过渡状态。
记忆点:
- 联调用
path,过渡用git,稳定复用用 crates.io。 - 发布前检查是否仍残留 path 依赖。
- 依赖来源会影响可复现性和发布流程。
Q11. Cargo feature 是模块系统的一部分吗?
Tags: common features
适用版本: Cargo features
一句话答案: 不是。feature 是构建期开关,用来控制依赖和条件编译;module 是命名空间,它们解决的问题完全不同。
解答: 很多人第一次接触 feature 时,会把它想成“按需加载模块”。更准确的说法是:feature 决定某段代码在这次构建里要不要参与编译,而 module 决定这些代码在 crate 里如何命名和暴露。
| |
| |
feature 设计应尽量“只加不减”,因为 Cargo 会对同一依赖做 feature 并集统一,不能指望不同下游把它编译成两份互斥版本。
Go 对比:
- Go 的 build tags 更像 feature 的一部分用途,但没有 Cargo 这种依赖图级别的统一机制。
- Rust feature 会影响依赖解析和
cfg条件编译,参与范围更大。 - 需要组织命名空间时别找 feature;那是 module 的工作。
记忆点:
- feature 管“编不编进来”。
- module 管“编进来以后怎么组织”。
- 不要把两者混成一个概念。
Q12. resolver = "2" 和 resolver = "3" 需要知道到什么程度?
Tags: common resolver
适用版本: resolver 2 自 Rust/Cargo 1.51+;resolver 3 自 1.84+
一句话答案: 日常使用只需记住:新 workspace 尤其是 Edition 2024 项目优先用 resolver = "3",它在现代依赖解析和 rust-version 兼容性上更合理。
解答: resolver 影响 Cargo 如何在整个依赖图里统一 features、选择兼容版本。对大多数业务代码来说,不必把实现细节背成面试题,但至少要知道它是 workspace 级设置,不能随便在子 crate 各写各的。
| |
如果你的项目还是旧 workspace,先确认团队工具链版本,再决定是否升级。升级 resolver 通常比升级 edition 风险小,但仍建议放进 CI 验证,而不是拍脑袋改。
Go 对比:
- Go 工具链里没有一个和 Cargo resolver 一一对应的日常显式旋钮。
- 这也是 Cargo 比 Go 暴露更多“依赖图控制面”的体现。
- 你不用天天改它,但知道它存在,能少踩很多 workspace 怪坑。
记忆点:
- resolver 是 workspace 级配置。
- 新项目优先
resolver = "3"。 - 版本升级前先看团队工具链和 CI。
Q13. Rust module 和 Go package 为什么老对不上号?
Tags: hot module package
适用版本: Rust 1.0+
一句话答案: Go 的 package 大致等于“一个目录里的编译命名空间”;Rust 把工程拆成 package(Cargo)、crate(编译单元)、module(命名空间)三层,拿 Go 的“目录=包”去套会处处错位。
解答: 对不上号通常不是语法记错,而是映射错了层级。Go 里你说“这个 package”,往往同时指目录、导入路径和可见性边界;Rust 里这三件事被拆开了。
| |
结果是:同目录多个文件不会自动进同一个模块;pub 也不等于“只要大写就全仓库可见”。你要先问“这是哪个 crate 的模块树”,再问“路径上每一层有没有 pub”。
Go 对比:
- Go:目录进 package,导入路径几乎就是目录约定。
- Rust:文件进模块树靠
mod声明;导入路径是模块路径,不是单纯文件夹名。 - 用 Go 心智读 Rust,最常见症状是“文件放对了却找不到符号 / 外部看不见”。
记忆点:
- 别把 Go package 一对一翻译成 Rust module。
- 先分清 package / crate / module。
- 可见性看模块路径上的
pub,不看文件名大小写。
Q14. 到底用 foo.rs 还是 foo/mod.rs?
Tags: common mod.rs
适用版本: Rust 1.0+;子模块文件布局自 Edition 2018 更灵活
一句话答案: 模块只有自己、没有子模块时用 foo.rs;需要 foo/bar.rs 这类子树时,用 foo.rs + foo/ 目录,或传统的 foo/mod.rs,二者等价,团队选一种即可。
解答: mod foo; 会找 foo.rs 或 foo/mod.rs,二选一,不要同时存在。现代风格更推荐“父模块用 foo.rs,子模块文件放进 foo/”,避免深层全是 mod.rs 不好搜索。
| |
有子模块时不要只建 net/ 却漏掉 net.rs/net/mod.rs:父模块文件才是 mod net; 的落点。
Go 对比:
- Go 没有“父文件声明子文件”这一步,同目录
.go文件自动同包。 - Rust 必须显式
mod,所以才有foo.rsvsfoo/mod.rs的选择。 - 选哪种布局不影响语义,影响的是可读性和文件搜索体验。
记忆点:
foo.rs与foo/mod.rs互斥,语义相同。- 有子树时优先
foo.rs+foo/。 - 先保证父模块文件存在,再谈子模块。
Q15. 为什么写了 mod 还要再 use?
Tags: hot mod use
适用版本: Edition 2018+
一句话答案: mod 把文件挂进模块树;use 只是在当前作用域给已有路径起短名。前者建树,后者省字,职责不同。
解答: 很多人把 mod 当成 Go 的 import。其实 mod foo; 之后,符号已经在 foo::...(或 crate::foo::...)下可用了;再写 use 只是为了少敲前缀,或者把深层路径提到当前作用域。
| |
反过来:只写 use crate::math::add; 却从没 mod math;(或从未在祖先模块声明),树里根本没有这个节点,use 也会失败。外部依赖同理——use serde::Serialize; 之前要先在 Cargo.toml 声明依赖,那是 crate 级引入,不是 mod。
Go 对比:
- Go 的
import同时完成“依赖这个包”和“把名字引进来”。 - Rust 拆成:依赖进
Cargo.toml,本 crate 文件进mod,短名再use。 - 记口诀:
mod建树,use取别名。
记忆点:
mod≠import。- 没
mod(或没依赖)就没有可use的路径。 use可省略,只是写法更啰嗦。
Q16. lib + 多个 bin 的目录怎么摆?
Tags: common lib bin
适用版本: Rust 1.0+
一句话答案: 共享逻辑放 src/lib.rs,默认入口放 src/main.rs,其余命令放 src/bin/*.rs;各 binary 通过 package 名调用 library,而不是互相 mod 抄代码。
解答: 这是单 package 多命令的标准骨架。和“只有多个 bin、没有 lib”相比,多一个 lib.rs 的好处是:业务逻辑可单测、可被多个入口复用,bin 文件只保留参数解析和进程边界。
| |
| |
| |
只有当某个 bin 路径/名字特殊时,才在 Cargo.toml 里写 [[bin]];常规 src/bin/*.rs 不必手写。命令长成独立产品后再拆 workspace。
Go 对比:
- 对应 Go 的
pkg/或内部库 +cmd/app1、cmd/app2。 - Rust 默认约定更死:
lib.rs/main.rs/bin/路径即约定。 - 共享代码进 lib,是避免多个
main互相复制的关键。
记忆点:
- lib 承载逻辑,bin 承载入口。
- 其他命令默认丢进
src/bin/。 - binary 用 package 名引用 library API。
Q17. 结构体字段的 pub 和模块的 pub 怎么配合?
Tags: hot pub struct visibility
适用版本: Rust 1.0+
一句话答案: 路径上每一层模块都要 pub,类型本身要 pub,字段还要单独标 pub 才能从外部读写;缺任何一环,外部都只能看见“有这个类型”或干脆看不见。
解答: 可见性是逐项、逐层的。pub struct User 只表示类型名可被路径指到;字段默认仍私有,外部不能写 user.id,通常要通过构造函数或方法访问。模块若未 pub,外面连这条路径都走不通(见 Q6)。
| |
| |
常见组合:
- 类型 pub + 字段全私有:最稳的封装,只暴露方法。
- 类型 pub + 部分字段 pub:DTO / 纯数据配置很常见。
- 模块不 pub + 类型 pub:只给父模块用,再由父模块
pub use精选导出。
Go 对比:
- Go 靠字段名首字母大小写控制导出,和文件所在 package 绑定。
- Rust 把“模块路径是否公开”和“字段是否公开”拆成两套开关。
- Go 程序员常漏掉“字段还要再写一次
pub”,结果类型能提到、字段全红。
记忆点:
- 模块路径、类型、字段,三层各自要
pub。 - 默认字段私有,不等于类型私有。
- 对外 API 优先“类型公开、字段私有”。
Q18. 为什么单元测试能测 private,集成测试不能?
Tags: hot unit-test integration-test privacy
适用版本: Rust 1.0+
一句话答案: 单元测试编译进同一个 crate,和被测代码共享私有可见性;tests/ 下的集成测试是另一个 crate,只能走你的公共 API。
解答: 写在 src/ 里、通常包在 #[cfg(test)] mod tests 中的测试,是当前 crate 的子模块,因此 use super::* 后能调用私有函数、读私有字段。tests/foo.rs 则像外部用户:use my_crate::...,看不见非 pub 项。这和“测没测到”无关,是编译单元边界决定的。
| |
| |
| |
实践上:内部算法、不变量用单元测试白盒覆盖;对外契约、示例用法用集成测试黑盒锁住。想测“半内部”API,用 pub(crate) 并在同 crate 单测,而不是为了测试强行 pub。
Go 对比:
- Go 的
_test.go同 package 可测未导出符号;package foo_test则只能测导出 API。 - Rust 的单元/集成测试大致对应这两种模式,但集成测试目录约定更死。
- 两边都成立:测试便利不能压过 API 边界设计。
记忆点:
- 同 crate → 能测 private。
tests/→ 外部 crate,只能 pub。- 别为了测试把内部细节全部公开。
Q19. #[cfg(test)] 模块该怎么组织?
Tags: common cfg(test) tests
适用版本: Rust 1.0+
一句话答案: 默认在被测模块文件末尾放 #[cfg(test)] mod tests { use super::*; ... };测试辅助代码也加 #[cfg(test)],需要跨文件共享时再拆 #[cfg(test)] mod test_support;。
解答: #[cfg(test)](configuration attribute,配置属性)保证这些模块只在 cargo test 时编译,正式 cargo build 不会带上断言和测试夹具。组织原则:测试离被测代码近、辅助代码不要泄漏进发布构建、避免和 tests/ 集成测试目录搞混。
| |
| |
多文件时常见摆法:src/lib.rs 写 #[cfg(test)] mod tests; 并配 src/tests.rs(注意这是源码树内测试子模块,不是根目录 tests/)。根目录 tests/*.rs 专留给跨 crate 集成测试(见 Q18)。
Go 对比:
- 近似
foo_test.go与同包辅助函数;Rust 用属性把测试代码从正式构建剔除得更干净。 - Go 没有“源码旁 cfg 模块 / 根目录 tests 集成箱”这套双轨目录约定。
- 共享测试夹具时两边都要避免污染生产包。
记忆点:
- 文件末尾:
#[cfg(test)] mod tests。 - 辅助模块同样要
cfg(test)。 src内单测 ≠ 根目录tests/集成测。
Q20. 模块互相 use 不上、像循环依赖时怎么改?
Tags: hot use module cycle
适用版本: Rust 1.0+
一句话答案: Rust 允许模块树里互相引用路径,但“类型/函数定义形成环”时要拆:把共享定义抽到第三模块,或让依赖单向流动(父用子、子通过参数/回调回传),不要两个子模块彼此硬耦。
解答: 报错常见形态是:a 里 use crate::b::...,b 里又 use crate::a::...,再在类型字段或默认实现里真正咬死对方,编译器就会在类型检查阶段抱怨。use 本身不是 Go 那种 package import cycle 的同款规则,但逻辑环一样难维护。改法优先“抽出共享层”。
| |
| |
若必须“子模块回调父逻辑”,用函数参数/Fn 闭包注入,或把编排放在父模块,而不是让子模块 use super::brother。也可合并成一个模块再 pub use 拆路径(见 Q7),避免为了目录好看切出真环。
Go 对比:
- Go 直接禁止 import cycle,编译器硬拒绝。
- Rust 更常见是类型层环或设计上的环;解法同样是抽
types/common、倒转依赖。 - 两边都成立:共享定义上移,业务依赖单向。
记忆点:
- 共享类型抽到第三模块。
- 依赖单向,编排放父层。
- 目录拆分服从依赖方向,不要反过来。