卷 VI · 落地CH 21深度 21/24

组织代码:模块、crate 与 cargo 生态

前面二十章讲的是「怎么写对一段代码」。从这一卷起,讲「怎么把一堆代码组织成能交付的工程」。第一件事是分而治之:mod 划分命名空间、pub 控制谁能看见谁、use 把长路径缩短。规模再大,用 crate 和 workspace 组织。而 crates.io 让你站在整个社区的肩膀上。

modcrate可见性workspace

先厘清四个层级的词

Rust 的代码组织有四个词,从小到大,先分清就不乱:

  • 模块(module):一个命名空间,用来分组和控制可见性。用 mod 声明,可以嵌套。它是组织代码的最小单位。
  • crate:一个编译单元。要么是二进制 crate(有 main,能跑),要么是库 crate(给别人用的代码,无 main)。一个 crate 内部可以有很多模块。
  • 包(package)cargo new 建出来的那个东西,由一个 Cargo.toml 管理,包含一个或多个 crate(通常一个库 + 若干二进制)。
  • 工作空间(workspace):把多个相关的 package 组织在一起、共享依赖和构建,适合大项目 / monorepo。

日常小项目,你就一个 package、一个 crate、几个模块,简单得很。词虽多,绝大多数时候你只跟「模块」打交道。

模块:mod、pub、use

模块用 mod 声明,划出一块命名空间。里面的东西默认私有,想让外面用,得显式 pub

mod network {
    pub fn connect() { /* 对外可见 */ }

    fn handshake() { /* 私有,只有 network 内部能用 */ }

    pub mod http {                    // 嵌套模块
        pub fn get(url: &str) { /* ... */ }
    }
}

fn main() {
    network::connect();               // 用 :: 走路径
    network::http::get("...");
}

路径长了,用 use 把它引进当前作用域,之后直接用短名:

use network::http;
// 之后可以直接写 http::get("..."); 不用每次写全路径

use std::collections::HashMap;        // 引入标准库的类型
let mut m = HashMap::new();

真实项目里,模块通常拆成文件mod network;(不带花括号)告诉编译器「去找 network.rsnetwork/mod.rs」。于是你的目录结构,天然就是模块树。src/main.rssrc/lib.rs 是 crate 的根。

可见性默认私有:又一处「安全默认」

注意上面那个细节:Rust 里一切默认私有,你得主动 pub 才对外暴露。这和很多语言(默认 public 或 package-private)相反,而它和这本书反复出现的主题一脉相承 —— 把「安全 / 保守」设成默认,把「开放 / 危险」设成需要显式动作。

这带来一个很实在的好处:一个模块 / 库的公开接口(API)是被精心圈定的一小块,其余全是能随便重构的内部实现。你重构私有部分时,编译器保证不会意外影响到外部调用者 —— 因为他们根本看不见那些私有的东西。这让「维护一个库而不破坏别人」变得可控。pub(crate)pub(super) 还能做更细的可见性控制(「只在本 crate 内公开」等)。

crates.io:站在社区的肩膀上

第 3 章提过 crates.io 是 Rust 的中央包仓库。这里值得强调它对「落地」的意义:你几乎不用重复造轮子。加依赖只要一行 cargo add,cargo 自动处理版本解析、下载、编译、锁定(Cargo.lock 保证团队和 CI 用完全一致的依赖版本)。生态里那些几乎是事实标准的库,值得记住:

要干的事常用 crate
序列化 / 反序列化(JSON 等)serde + serde_json
异步运行时tokio
HTTP 客户端 / Web 框架reqwest / axum
错误处理anyhow(应用)/ thiserror(库)
命令行参数解析clap
日志 / 追踪tracing
随机数、正则、时间rand / regex / chrono

这些库的质量普遍很高 —— 部分原因正是这本书讲的一切:类型安全和所有权让库作者能设计出「误用即编译错误」的 API,你用起来踏实。

◆ 一个务实的组织建议

刚起步别过度设计模块结构。一个文件写到几百行了、或者某块逻辑明显自成一体了,再拆成模块 / 文件。让结构跟着代码长,而不是一开始就搭一堆空目录。cargo 和模块系统足够灵活,重构模块结构是很轻的操作(编译器会揪出所有路径错误)。等项目真大到需要拆库了,再上 workspace。

回流

⟲ 回流 · 模块与包,你早有对应物

Java / Kotlin:Rust 的 mod ≈ package(命名空间 + 可见性),useimport,crate ≈ 一个 jar / 模块,workspace ≈ Gradle 的多模块项目,crates.io ≈ Maven Central。最大的差别是可见性默认值:Java 默认 package-private,Rust 默认完全私有、且模块可以任意嵌套并精细控制(pub(crate) 等),比 Java 的 public/protected/private/package 四档更灵活。另一个差别是 Rust 的模块树不强制对应目录结构(虽然通常对应),比 Java「包名必须等于目录路径」宽松。

JS / TS:Rust 的 useimport,crate ≈ 一个 npm 包,cargo ≈ npm,crates.io ≈ npm registry,Cargo.lockpackage-lock.json。区别是 Rust 的模块可见性是语言强制的(pub),而 JS 靠 export 约定 + 打包工具,且 Rust 没有 JS 那种「一个包能有几十个传递依赖、还常打架」的依赖地狱 —— cargo 的版本解析和编译期检查让依赖管理稳当得多。

这一章的一句话

mod 分命名空间、pub 控可见性(默认私有,又一处安全默认)、use 缩短路径;crate 是编译单元、workspace 组织大项目;crates.io 让你站在社区肩上,cargo 一行命令搞定依赖。让结构跟着代码长,别过度设计。

代码组织好了,怎么保证它真的对、别人会用?下一章讲 Rust 工程化的三件套:测试、错误处理、文档 —— 每一件都有让你惊喜的内建支持。