阶段六 · 错误处理与工程组织

包,Crate 与模块系统

一句话总结

Rust 用四层概念组织代码:Package(包)→ Crate(编译单元)→ Module(模块)→ 条目.
cargo 贯穿始终,角色相当于 Go 的 go 命令 + go.mod.
本篇厘清这套层级,模块拆分,可见性与测试组织,并对照 Go 的 module/package.

四层组织结构

初学者最容易混淆 package,crate,module 的关系,先用一张表锚定:

Rust 概念 含义 Go 类比
Package 一个 Cargo.toml 管理的项目,可含多个 crate 一个 module(go.mod)
Crate 一次编译的根单元,分二进制 crate 和库 crate 最终的可执行程序 / 库
Module crate 内部的命名空间,控制可见性 Go 的 package
条目(item) 函数,结构体,Trait 等 同名概念

命名陷阱:Rust 的 module 约等于 Go 的 package

这是 Go 程序员最易踩的术语错位.Go 的 "package"(代码组织/可见性单元)对应的是 Rust 的 module,而不是 Rust 的 "package".

Rust 的 "package" 更接近 Go 的 "module"(go.mod 项目).记住这层错位,后面就不乱了.

两种 crate

  • 二进制 crate(binary):有 main 函数,编译成可执行程序.入口 src/main.rs.
  • 库 crate(library):没有 main,供其他 crate 引用.入口 src/lib.rs.

一个 package 最多一个库 crate,但可有多个二进制 crate(放在 src/bin/ 下).这比 Go "一个目录一个 package" 的规则更灵活.一个常见的项目结构是"库 + 薄二进制":核心逻辑放 lib.rs,main.rs 只做命令行入口,调用库--这样核心逻辑可被测试,也可被别的 crate 复用.

模块:用 mod 划分命名空间

模块在 crate 内部组织代码,控制可见性.可以内联定义:

// src/lib.rs
mod network {
pub fn connect() {
println!("连接中");
}

pub mod server {
pub fn start() {
println!("启动服务");
}
}
}

fn main() {
network::connect(); // 路径用 :: 分隔
network::server::start();
}

把模块拆成文件

模块变大就该拆成独立文件.mod network;(不带花括号)告诉编译器去对应文件找内容.这点和 Go 按目录自动归集不同--Rust 要显式声明模块树:

src/
├── main.rs 声明 mod network; → 去 network.rs 找
├── network.rs 声明 pub mod server; → 去 network/server.rs 找
└── network/
└── server.rs 实际内容
// src/main.rs
mod network; // 引入 network.rs 作为模块

fn main() {
network::connect();
}

// src/network.rs
pub mod server; // 进一步引入 network/server.rs

pub fn connect() { println!("连接中"); }

可见性:pub 与默认私有

Rust 的默认可见性是私有:模块里的条目默认只在本模块(及子模块)可见,要对外暴露必须加 pub:

Go Rust
导出规则 首字母大写即导出 显式加 pub
默认 小写 = 包内私有 pub = 模块私有
粒度 包级 可细到 pub(crate),pub(super)
mod store {
pub struct Item {
pub name: String, // 字段也要单独 pub 才对外可见
price: f64, // 私有,外部访问不到
}

pub(crate) fn internal() {} // 仅本 crate 内可见
}

Go 靠"首字母大小写"控制导出,简洁但只有"导出/不导出"两档.
Rust 用 pub 关键字,虽啰嗦,却能精细到 pub(crate)(仅本 crate),pub(super)(仅父模块)等多档,适合大型项目精确控制 API 边界.

use:引入路径

每次写全路径 network::server::start() 很累.use 把路径引入当前作用域,类似 Go 的 import:

use std::collections::HashMap;        // 引入后可直接写 HashMap
use network::server::start;

use std::io::{self, Read, Write}; // 一次引入多个
use std::collections::HashMap as Map; // 重命名,类似 Go 的 import m "..."

路径关键字与 prelude

  • crate:: -- 从当前 crate 根开始(类似绝对路径)
  • self:: -- 当前模块;super:: -- 父模块(类似 ../)

另外,String,Vec,Option 从没 use 过却能直接用,是因为它们在标准库 prelude 里--一组自动引入每个程序的常用条目,类似 Go 内置的 len,make.

测试组织:就写在源码里

Rust 的测试是语言内建的,组织方式很有特色--单元测试通常和被测代码写在同一文件,放在一个带 #[cfg(test)] 的模块里(该模块只在测试时编译):

pub fn add(a: i32, b: i32) -> i32 {
a + b
}

#[cfg(test)] // 只在 cargo test 时编译
mod tests {
use super::*; // 引入父模块的内容

#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
测试类型 位置 能测什么
单元测试 源码文件内 #[cfg(test)] mod tests 含私有函数(同模块可见)
集成测试 项目根的 tests/ 目录 只能测公开 API(像外部用户)

运行用 cargo test(第 01 篇见过).和 Go 把测试放在 _test.go 文件不同,Rust 单元测试就嵌在源码里,use super::* 让它能访问同模块的私有项--这是 Rust 能直接测私有函数的原因.

工作空间:管理多 crate 项目

大型项目可用 workspace 把多个相关 crate 组织在一起,共享依赖与构建缓存,类似 Go 的 go.work:

# 顶层 Cargo.toml
[workspace]
members = ["app", "core", "utils"]

入门阶段单 crate 足够,等项目变大,需要拆分库时再了解 workspace 即可.

快速回顾

  • 四层结构:Package → Crate → Module → 条目;注意 Rust 的 module 约等于 Go 的 package.
  • 两种 crate:二进制(main.rs)与库(lib.rs);常见"库 + 薄二进制"结构便于测试与复用.
  • 模块树需显式声明:mod xxx; 引入文件,而非按目录自动归集.
  • 可见性:默认私有,pub 导出,支持 pub(crate) 等精细档位,比 Go 大小写更细.
  • use:引入路径;crate::/self::/super:: 定位;prelude 自动引入常用类型.
  • 测试:单元测试用 #[cfg(test)] mod tests 嵌在源码里(可测私有),集成测试放 tests/;cargo test 运行.

动手练习

  1. 内联模块:在 main.rs 内联定义一个含嵌套子模块的 mod,用全路径调用其中的 pub fn.
  2. 文件拆分:把上题模块拆到独立文件,用 mod xxx; 声明,验证仍能调用.
  3. 可见性实验:给模块里的函数去掉 pub 观察可见性报错,再改成 pub(crate) 对比.
  4. 单元测试:给一个函数写 #[cfg(test)] mod tests 单元测试,用 cargo test 跑通,并验证它能调用私有函数.
  5. use 与别名:用 use 引入一个标准库类型并用 as 重命名,在代码中使用别名.

基础系列完成

至此走完 Rust 基础的全部地基:工具链,变量,所有权与借用,复合类型,控制流,集合与函数式,泛型与特征,错误处理与工程组织.

接下来进入**「Rust 进阶 · 生命周期与并发」**分类,正式攻克生命周期,智能指针与多线程这些 Rust 最难也最核心的部分.