本文永久链接 – https://tonybai.com/2026/10/28/google-rust-style-guide-large-scale-engineering
大家好,我是Tony Bai。
【导读】
Rust 社区有一条广为人知的原则:代码应该遵循社区约定,格式交给 rustfmt,API 设计则参考 Rust API Guidelines。
但如果你需要在一个拥有数百万行代码、多个语言共存、依赖关系复杂,并且必须长期维护的超大型代码库中使用 Rust,仅仅遵循这些规则就足够了吗?
Google 给出了自己的答案。
2026 年 10 月,Google 公开了 Rust Style Guide。这份指南的特别之处在于,它并不满足于规定缩进、命名和格式,而是将代码组织、所有权设计、API 演进、错误处理、宏使用和可维护性纳入统一的工程规范。
其中有些建议相当反直觉:优先拆分小 Crate、尽量避免 Rc 和 Arc、不要为了组织代码而引入 Trait、公共枚举优先考虑 #[non_exhaustive],甚至在 Chromium 中明确禁止使用 async/.await。
这些规定并非都适用于每个 Rust 项目。它们背后有着具体的工程环境和取舍。
指南发布后,Hacker News 上也迅速出现了争论。开发者围绕小 Crate 是否会导致过度拆分、#[non_exhaustive] 是否削弱编译器检查能力、类型导入方式是否影响可读性等问题展开讨论。
真正值得研究的,并不是我们应不应该照抄 Google 的规范,而是一个更深层的问题:当软件的主要成本从编写代码转向理解、修改和维护代码时,一家超大规模工程组织会如何重新定义好的代码?
【文章要点】
- Google 的 Rust Style Guide 不只是格式规范,更是大规模工程治理规则。它强调代码可读性、长期可维护性和自动化重构能力,尤其关注跨大型代码库执行批量变更(LSC)的成本。
- 优先拆分小 Crate,建立清晰的依赖边界。Chromium 倾向于使用细粒度 Crate,以改善依赖管理、并行编译和构建缓存;但这是一项有特定工程背景的建议,并不意味着所有 Rust 项目都应该无限拆分。
- 优先使用借用和唯一所有权,避免过早引入
Rc、Arc和深拷贝。共享所有权应该源于真实需求,而不是成为绕过所有权设计问题的默认手段。 - 设计更灵活、更清晰的 API。优先使用
&str、&[T]等通用借用类型;通过enum表达明确的业务状态,并合理区分Option、Result与panic!的适用场景。 - 为公共 API 的未来演进预留空间。Google 建议对跨团队使用的公共枚举和特定公共结构体考虑使用
#[non_exhaustive],以降低新增变体或字段导致大规模兼容性问题的风险,同时也需要权衡穷尽匹配带来的编译期保障。 - 克制使用 Trait、宏和复杂抽象。优先采用普通函数、类型和模块;只有在确实需要代码生成、泛型约束或运行时多态时,才引入相应机制。抽象应当降低理解成本,而不是增加额外的语言复杂度。
- 部分规则是特定代码库的工程选择。例如,Chromium 明确限制
async/.await的使用。这反映的是特定项目的运行时和基础设施约束,而不是 Rust 异步编程的普遍缺陷。 - Hacker News 的争论揭示了工程设计中的真实权衡。小 Crate 与过度拆分、API 演进与穷尽匹配、类型导入与可读性,都没有脱离上下文的唯一答案。
- 普通 Rust 项目应借鉴原则,而非照搬全部规则。可以优先采用格式检查、标准 Trait、清晰文档和合理借用等低成本实践;至于 Crate 粒度、公共 API 兼容策略和并发模型,则应结合自身项目规模与构建环境决定。

Google 为什么需要一份自己的 Rust 风格指南?
先看一个容易被忽略的事实:Google 并不是在重新发明 Rust 的编码规范。
官方指南明确要求,除非与 Google 的规则冲突,否则应遵循 Rust 社区公开的风格规范,包括官方 Rust Style Guide 和 Rust API Guidelines。
那么,既然社区已经有了规范,Google 为什么还要额外制定一套规则?
答案藏在它的工程规模中。
Google 的 Rust 代码分布在多个工程领域,包括 Google 内部代码库、Android、ChromeOS、Fuchsia,以及 Chromium 等项目。不同团队有各自的历史、工具链和代码组织方式。即使大家使用同一种语言,也可能对模块划分、API 设计、错误处理和依赖管理形成不同习惯。
在小型项目中,这种差异通常不会造成严重问题。但在超大型代码库中,局部的设计选择可能产生全局影响。
例如,一个看似方便的公共 API,可能在未来每次修改时都要求大量调用方同步调整;一个庞大的公共工具库,可能让只需要字符串处理的程序也间接依赖大量无关功能;一种需要人工理解的代码生成模式,则可能让跨目录、跨模块的自动化重构变得困难。
Google 的指南因此提出了几个明确目标:
- 可读性与可维护性优先于编写时的便利。写代码时多做一点约束,如果能够降低未来的理解成本,就值得考虑。
- 尽可能有利于自动化。统一的代码结构能够帮助工具执行大规模修改。
- 尽量与 Rust 社区保持一致。只有能够产生实际收益时,才值得引入额外规则。
- 每条规则都应该有价值。规范不应仅仅为了体现严格而增加开发者的负担。
这里最值得关注的是第二点:大规模变更,英文称为 Large-Scale Changes,简称 LSC。
在大型工程中,修改代码并不总是一个开发者编辑一个文件。可能需要统一更改公共 API、迁移依赖、调整类型定义,甚至对整个代码库执行机械化重构。
当这种工作成为日常活动时,代码风格就不再只是审美问题。
它会直接影响自动化工具能否准确理解代码、修改是否容易验证,以及一次看似局部的变更会影响多少下游组件。
可以把 Google 的思路概括为:
代码风格的一致性 ↓ 更明确的结构和边界 ↓ 更容易分析与自动化修改 ↓ 降低大规模变更的成本与风险 ↓ 提升长期维护效率 这也是理解整份指南的起点。
第一条重要分歧:为什么 Google 偏爱小 Crate?
如果要从整份指南中选出最容易引发争论的建议,Chromium 的小 Crate 策略无疑是其中之一。
Google 在 Chromium 的工程规范中建议优先使用小 Crate,理想情况下,一个 Crate 甚至可以只包含一个源文件,并且聚焦于单一抽象。
同时,Chromium 规定,同一个 rust_library 对应的 Rust 源文件应放在同一个目录下,不应通过创建子目录来组织这个 Crate 内部的文件。
这与很多 Rust 开发者熟悉的项目结构并不完全相同。
传统 Rust 项目的组织方式
使用 Cargo 开发应用时,我们通常会先创建一个 Crate,再通过模块(mod)划分代码。
例如:
my-app/ ├── Cargo.toml └── src/ ├── main.rs ├── config.rs ├── network.rs ├── storage.rs └── metrics.rs 当项目变大时,还可以进一步创建子模块:
src/ ├── main.rs ├── network/ │ ├── mod.rs │ ├── client.rs │ └── retry.rs └── storage/ ├── mod.rs └── database.rs 这种结构很自然。模块可以表达逻辑边界,开发者不必为每个小组件创建独立的 Cargo 包。
但 Google 的工程环境中,Crate 的粒度还有另一层意义。
在大型代码库中,Crate 是构建边界
Rust 的 Crate 是编译单元。大型构建系统通常还会将其作为组织依赖、执行并行构建和管理构建缓存的重要边界。
假设一个大型项目拥有下面这样的依赖关系:

应用可能只需要网络模块,却依赖了整个 common Crate。
随着项目发展,射频模块的修改也可能波及两者共享的构建边界,依赖关系变得越来越难以控制。实际的重新编译范围还取决于构建系统、缓存和具体修改,不能简单地认为任何源文件变更都会导致整个 Crate 全量重编译,但粗粒度边界确实可能带来额外成本。
如果将不同职责拆成独立 Crate,依赖关系就可以变得更加明确:

这样,应用只需声明真正需要的依赖,射频模块也不再天然成为网络工具的组成部分。
小 Crate 还可以带来几个额外好处:
- 限制跨组件的依赖关系。
- 缩小公共 API 的范围。
- 避免模块之间形成难以察觉的依赖循环。
- 让构建系统拥有更细粒度的并行构建和缓存机会。
- 让组件能够独立理解、测试和演进。
Hacker News 上有开发者指出,在一些大型构建系统中,创建一个独立库并不一定需要像普通 Cargo 项目那样承担完整的目录和清单管理成本。Crate 因而可以成为一种轻量级的架构边界,而不仅仅是代码打包方式。
另一些讨论则强调,模块依赖与 Crate 依赖并不完全相同。Rust 模块内部仍然可以组织复杂的数据结构和抽象,拆分 Crate 不等于要求所有函数都缩短到一两行。
这一区别非常重要。
Google 倡导的是更细粒度的库和依赖边界,而不是把所有抽象都切碎。
小 Crate 不等于无限拆分
Google 的规则也没有把小 Crate 绝对化。
指南明确指出,Trait coherence 等规则可能要求某些类型和 Trait 保持在同一个库中。基础库即使规模较大,也可能是合理的。
换句话说,拆分 Crate 不是一项脱离语言语义的机械任务。
Rust 的孤儿规则限制了我们能够为哪些类型实现哪些 Trait。如果把类型和相关 Trait 拆到不合适的 Crate 中,原本自然的实现可能变得不合法,或者迫使开发者设计出更复杂的架构。
因此,Google 给出的顺序是:先判断 API 是否真的应该属于同一个 Crate,再考虑是否需要通过模块组织实现细节。
这与一种常见的过度设计形成了鲜明对比:先构造复杂的模块树,再希望这些模块天然拥有良好的边界。
对于 Google 而言,真正重要的边界是构建和依赖边界,而不是目录树看起来是否足够漂亮。
需要强调的是,这些关于小 Crate 和扁平文件布局的规则带有明确的 [chrome] 标记,主要针对 Chromium 的工程环境,并非 Google 要求所有 Rust 项目都采用同样的文件布局。
对于普通 Cargo 项目,一个拥有清晰模块结构的中型 Crate 完全可能是更好的选择。
少用 Rc、Arc 和 clone():所有权设计应该从简单开始
Rust 最有特色的机制之一,就是通过所有权和借用检查,在编译阶段发现许多内存安全问题。
但 Rust 开发者也很容易形成一种习惯:遇到所有权问题,就尝试使用 Rc、Arc 或 clone() 让代码先编译通过。
Google 的指南对此给出了明确建议:优先考虑唯一所有权和借用,而不是过早引入共享所有权。
为什么 Arc 不应该成为默认选项?
看一个完整、可运行的例子。
下面的代码将一份只读配置传递给多个函数:
struct Config { service_name: String, timeout_ms: u64, } fn print_service(config: &Config) { println!("Service: {}", config.service_name); } fn print_timeout(config: &Config) { println!("Timeout: {} ms", config.timeout_ms); } fn main() { let config = Config { service_name: "payment".to_string(), timeout_ms: 3000, }; print_service(&config); print_timeout(&config); } 这里没有使用 Arc,也没有复制整个配置对象。
两个函数通过不可变引用读取同一个对象。只要调用关系和生命周期允许,Rust 就能够安全地管理这种访问。
现在,如果开发者习惯性地使用 Arc<Config>,代码可能变成这样:
use std::sync::Arc; struct Config { service_name: String, timeout_ms: u64, } fn print_service(config: &Arc<Config>) { println!("Service: {}", config.service_name); } fn main() { let config = Arc::new(Config { service_name: "payment".to_string(), timeout_ms: 3000, }); print_service(&config); } 这段代码同样可以运行,但它并没有体现共享所有权的必要性。
Arc 的核心用途,是允许多个所有者共同持有一个对象,并通过原子引用计数管理其生命周期。它本身不意味着对象的内部数据可以任意并发修改,也不能自动解决所有线程安全问题。
如果程序只是在一个明确的调用链中读取配置,那么普通借用通常更简单。
Google 希望开发者先问一个问题:我们真的需要多个独立的所有者,还是只需要在适当的生命周期内访问同一个对象?
两者并不是一回事。
clone() 不是借用问题的通用解法
另一个常见问题,是靠复制数据绕开借用检查带来的困难。
例如:
fn print_name(name: &String) { println!("{name}"); } fn main() { let name = String::from("Rust"); print_name(&name.clone()); } 这段代码可以编译,但为了让函数读取字符串,我们没有必要复制整个 String。
更符合 Rust 惯例的方式是使用 &str:
fn print_name(name: &str) { println!("{name}"); } fn main() { let name = String::from("Rust"); print_name(&name); print_name("Google Rust"); } 这个版本更加灵活。调用者可以传入已有 String 的借用,也可以直接传入字符串字面量。
Google 的指南进一步建议,在函数接收不可变数据时,尽可能接受更通用的借用类型。
| 不太灵活的参数类型 | 更灵活的参数类型 |
|---|---|
&String | &str |
&Vec<T> | &[T] |
&Box<T> | &T |
&Option<T> | Option<&T> |
例如,函数需要读取一组整数时:
fn sum(values: &[i32]) -> i32 { values.iter().sum() } fn main() { let numbers = vec![1, 2, 3, 4]; println!("{}", sum(&numbers)); println!("{}", sum(&[10, 20, 30])); } 这里的 sum 不关心调用者使用的是 Vec<i32>,还是其他能够提供切片的数据结构。
这不仅减少了不必要的类型约束,也避免了让函数接口与某种具体容器绑定。
当然,Google 并没有要求绝对禁止 clone()。
当确实需要独立拥有一份数据时,复制就是合理的选择。如果借用关系已经变得过度复杂,复制有时也可能是更清晰的工程决策。对于 Rc 和 Arc,指南同样保留了在其他所有权方案难以理解或维护时使用它们的空间。
关键不是把某个 API 列入黑名单,而是建立正确的默认顺序:
- 先考虑是否可以通过借用解决问题。
- 再判断是否确实需要独立所有权。
- 如果需要多个所有者,再选择适当的共享所有权机制。
- 最后根据实际需求决定是否复制数据。
Rust 的所有权系统不应该只是一个需要绕过去的编译器障碍,它本身就是帮助我们设计程序结构的工具。
Google 对 API 的态度:不要把未来的复杂性提前塞进今天的代码
Google 的指南并不只是关心函数实现,还特别强调 API 设计。
不过,它首先区分了两种代码:库代码与应用代码。
库代码通常需要面对多个使用者,设计者很难控制未来的调用方式,因此需要更加重视稳定性、通用性和演进空间。
应用代码则主要服务于特定业务。对于应用,指南明确建议避免过早泛化和抽象,在满足当前需求的前提下优先保持简单、直接。
这意味着 Google 并不是要求所有代码都设计成高度通用的框架。
相反,它试图避免两种方向相反的错误:公共库设计不足,导致未来难以演进;业务代码过度抽象,导致当前需求也难以理解。
为什么函数参数更喜欢 &str 和 &[T]?
前面已经展示了借用类型的优势。对于公共 API,这种设计还具有更深层的意义。
假设一个库只接受 &String:
fn normalize_name(name: &String) -> String { name.trim().to_lowercase() } 虽然函数本身能够工作,但它把调用方限制在一个不必要的类型要求上。
改成下面这样:
fn normalize_name(name: &str) -> String { name.trim().to_lowercase() } fn main() { let owned = String::from(" Rust "); println!("{}", normalize_name(&owned)); println!("{}", normalize_name(" Google ")); } 调用者可以使用不同的数据来源,而不需要先把所有输入统一转换成 String。
这种设计体现了 API 的一个基本原则:约束应该来自业务语义,而不是实现细节。
enum 优于 bool:让接口表达真正的意图
另一个很值得关注的建议,是在 API 中优先使用 enum,而不是含义模糊的 bool。
例如,假设一个日志系统需要决定是否记录详细信息。
一个简单的接口可能是:
fn configure_logging(verbose: bool) { if verbose { println!("Verbose logging enabled"); } else { println!("Normal logging enabled"); } } fn main() { configure_logging(true); } 问题在于,调用者看到 true 时,必须知道它究竟代表什么。
当参数变多时,问题会更加明显:
configure_logging(true, false, true); 这三个布尔值究竟对应哪些选项?调用方很难一眼看明白。
如果使用枚举:
#[derive(Debug, Clone, Copy)] enum LogLevel { Normal, Verbose, Debug, } fn configure_logging(level: LogLevel) { match level { LogLevel::Normal => println!("Normal logging enabled"), LogLevel::Verbose => println!("Verbose logging enabled"), LogLevel::Debug => println!("Debug logging enabled"), } } fn main() { configure_logging(LogLevel::Verbose); } 现在,接口明确表达了业务语义。
调用者不必查阅文档,也不需要猜测布尔值的含义。
枚举还有一个重要优势:它可以将一组相关状态集中到类型定义中,让编译器帮助检查状态处理逻辑。
需要注意,这并不意味着所有布尔参数都应该改成枚举。如果一个布尔值本身具有清晰的语义,例如 is_empty 的返回值,那么继续使用 bool 完全合理。
Google 的真正意图,是让公共 API 的表达能力与其业务含义相匹配。
Option、Result 和 panic! 应该各司其职
Google 对错误处理也给出了清晰的分类。
Option<T>:操作可能失败,但调用者不需要知道具体原因。Result<T, E>:操作可能失败,而且调用者需要知道错误原因,并有机会进行恢复。panic!:程序遇到了违反不变量的情况,通常意味着存在应该修复的程序缺陷。
下面是一个完整示例:
use std::num::ParseIntError; fn parse_port(input: &str) -> Result<u16, ParseIntError> { input.parse::<u16>() } fn find_port(config: &[(&str, &str)], key: &str) -> Option<u16> { config .iter() .find(|(name, _)| *name == key) .and_then(|(_, value)| value.parse::<u16>().ok()) } fn main() { match parse_port("8080") { Ok(port) => println!("Port: {port}"), Err(error) => println!("Invalid port: {error}"), } let config = [("http", "8080"), ("admin", "9000")]; println!("HTTP: {:?}", find_port(&config, "http")); println!("Missing: {:?}", find_port(&config, "missing")); } 这里,parse_port 保留了错误信息;find_port 则将查找不到配置和解析失败统一视为没有可用结果。
这两种接口服务于不同的需求。
在真实系统中,如果端口配置来自用户输入、配置文件或网络请求,通常应该保留足够的错误信息,以便调用者诊断和恢复。相反,如果某个条件属于程序自身必须满足的不变量,那么通过 panic! 暴露缺陷可能比悄悄返回一个默认值更合适。
指南也承认,不同代码库可能对 panic 有更严格的限制。
因此,重点不是简单地选择一种错误处理风格,而是明确每个 API 对失败的承诺。
一个容易引发争议的规则:公共枚举为什么应该考虑 #[non_exhaustive]?
这是 Hacker News 讨论中最值得深入研究的主题之一。
Google 建议,对公开暴露的枚举使用 #[non_exhaustive]。
一些开发者认为,这会削弱 Rust 枚举最有价值的特性之一:当库增加一个新变体时,编译器能够提醒调用者更新所有相关的 match 分支。
这个担忧并非没有道理。
要理解分歧,首先需要区分两个不同的问题:编译器能否检查调用者是否处理了当前所有情况,以及库作者能否在不破坏下游代码的前提下增加新的情况。
这两个目标并不总是一致。
没有 #[non_exhaustive] 时会发生什么?
假设一个库定义了下面的枚举:
pub enum Status { Ready, Busy, } 另一个项目可能这样使用它:
match status { Status::Ready => println!("ready"), Status::Busy => println!("busy"), } 因为这个枚举已经被完整匹配,所以调用方可以依赖编译器检查所有变体。
但是,如果库作者后来增加一个变体:
pub enum Status { Ready, Busy, Offline, } 下游代码可能需要修改才能重新编译。
从调用方的角度看,这是一项有价值的保护机制。新状态出现时,编译器迫使开发者重新审视原来的处理逻辑。
但从公共库作者的角度看,这也意味着增加一个变体可能成为破坏性变更。
#[non_exhaustive] 改变了什么?
如果库作者在公开枚举上添加这个属性:
#[non_exhaustive] pub enum Status { Ready, Busy, } 外部 Crate 在匹配这个枚举时,就必须考虑未来出现额外变体的可能性。
例如:
match status { Status::Ready => println!("ready"), Status::Busy => println!("busy"), _ => println!("other status"), } 这使得库作者未来增加变体时,不必要求所有下游项目都显式列出新情况。
但代价也很明确:调用者可以使用通配分支,从而不再被迫针对每一个新变体编写独立处理逻辑。
如果新变体代表一种需要特别处理的安全状态、业务状态或错误类型,那么通配分支可能掩盖本应在编译时暴露的问题。
因此,HN 上的反对意见抓住了一个真实的权衡:API 的演进自由度,与调用者对每种状态进行显式处理的能力,存在张力。
那么,Google 为什么仍然倾向于这种设计?
原因在于公共库的演进方式。
如果一个枚举属于内部应用,调用方和定义方通常由同一个团队控制,那么强制穷尽匹配可能非常有价值。
但如果这是一个面向大量外部使用者的公共库,库作者无法保证每个调用方都能及时升级和修改代码。此时,预留未来增加变体的空间,可以降低库升级时的协调成本。
两种设计并没有绝对的胜负。
| 使用场景 | 更值得考虑的方向 |
|---|---|
| 内部应用,所有调用方都由同一团队维护 | 强穷尽匹配 |
| 公共库,预计未来会增加枚举变体 | #[non_exhaustive] |
| 每个变体都需要独立、安全的业务处理 | 谨慎使用通配分支 |
| 状态集合长期稳定,且新增状态应触发全局审查 | 保留显式穷尽匹配的价值 |
还需要补充一个细节:#[non_exhaustive] 不仅可以用于枚举,也可以用于公开结构体。对于带有公开字段的结构体,它同样能够为未来增加字段预留空间。
不过,这个属性并不意味着结构体或枚举可以随意修改。类型的可见性、字段访问方式、构造方式以及具体变更仍然会影响兼容性。
真正值得借鉴的,是 Google 将 API 演进成本纳入类型设计的思路。
Rust 的类型系统能够帮助我们尽早发现问题,但公共库作者也必须考虑,哪些问题应该在编译时交给调用者处理,哪些变化应该由库本身吸收。
少用 Trait、宏和复杂抽象:不是所有扩展能力都值得保留
Rust 拥有非常强大的抽象能力。
Trait 可以描述行为,泛型可以提供静态多态,dyn Trait 可以提供动态分发,宏则允许开发者在编译阶段生成代码。
这些能力是 Rust 的重要优势,但 Google 的指南提醒开发者:强大的语言能力不意味着应该尽可能多地使用它们。
不要仅仅为了组织代码而定义 Trait
假设我们有一个简单的结构体:
struct User { name: String, } impl User { fn greeting(&self) -> String { format!("Hello, {}!", self.name) } } fn main() { let user = User { name: "Rust".to_string(), }; println!("{}", user.greeting()); } 这段代码不需要任何 Trait。
如果只是为了把 greeting 方法放到一个看起来更加抽象的接口中,我们可能会写出额外的 Trait 定义和实现。
但如果系统没有真正需要泛型约束、impl Trait、dyn Trait 或扩展 Trait,那么这种抽象可能只会增加理解成本。
Google 的规则是:当我们需要为一个类型实现功能时,默认优先考虑固有实现,也就是 impl Type;只有当 Trait 能够解决明确的问题时,才引入 Trait。
在库代码中,指南还倾向于优先使用泛型和 impl Trait,而不是无条件地选择 dyn Trait。这是因为泛型通常可以让编译器进行静态分发和更多优化,而动态分发会引入额外的间接调用。
当然,dyn Trait 仍然有它的适用场景。例如,需要在同一个集合中保存不同具体类型的对象,或者确实需要运行时多态时,动态分发可能更加合适。
因此,这条建议并不是拒绝抽象,而是要求抽象服务于明确的需求。
为什么 Google 对宏更加谨慎?
Google 对代码生成和元编程给出了相当明确的优先顺序:
- 普通函数、类型、Trait、对象和模块。
macro_rules!宏。- 其他代码生成方案。
- 过程宏。
原因并不难理解。
普通 Rust 代码更容易被 IDE、代码搜索工具和重构工具识别。宏则可能隐藏实际生成的代码,让开发者必须理解额外的语法和展开规则。
下面是一个简单的例子:
macro_rules! make_message { ($name:expr) => { format!("Hello, {}!", $name) }; } fn main() { let message = make_message!("Rust"); println!("{message}"); } 这段代码完全合法,也没有任何实际问题。
但如果需求只是生成一条简单的字符串,普通函数通常已经足够:
fn make_message(name: &str) -> String { format!("Hello, {}!", name) } fn main() { let message = make_message("Rust"); println!("{message}"); } 后者的调用关系和参数类型都更加直观。
这并不意味着应该禁止宏。宏能够解决普通函数无法解决的问题,例如生成重复的结构、扩展语法,或者在编译阶段完成某些特定任务。
Google 的重点是:当普通语言机制已经足够时,不要仅仅为了减少几行重复代码而创建一套新的小语言。
这个原则对于代码审查和自动化工具尤其重要。
在一个大型项目中,宏的使用方式可能形成独特的代码方言。开发者不仅需要理解 Rust,还需要理解项目内部的宏系统。跨模块重构时,工具也可能需要额外处理宏展开与源代码之间的关系。
对于规模较小的项目,这些成本可能无关紧要;对于长期维护的大型代码库,它们则可能逐渐累积。
迭代器链也不是越长越好
Google 对抽象的谨慎还体现在一个容易被忽略的地方:当直接循环更清晰时,不要强行使用迭代器组合链。
Rust 社区经常鼓励使用 map、filter、fold 等迭代器方法。这种风格非常适合表达简单的数据转换。
但当一个表达式包含复杂的条件判断、多个中间状态和嵌套闭包时,代码可能反而变得难以理解。
例如,下面的代码使用普通循环计算所有正整数的平方和:
fn sum_of_positive_squares(values: &[i32]) -> i64 { let mut total = 0_i64; for &value in values { if value > 0 { total += i64::from(value) * i64::from(value); } } total } fn main() { let values = [-2, -1, 0, 1, 2, 3]; println!("{}", sum_of_positive_squares(&values)); } 这段代码的意图非常明确:遍历、判断、累加。
如果问题很简单,使用迭代器也完全合理:
fn sum_of_positive_squares(values: &[i32]) -> i64 { values .iter() .copied() .filter(|value| *value > 0) .map(|value| i64::from(value) * i64::from(value)) .sum() } 这两个版本都可以编译运行。哪个更好,取决于具体上下文。
Google 的建议不是要求大家放弃函数式编程,而是避免把某一种代码风格当成必须遵循的仪式。
好的抽象应该让代码更容易理解,而不是让代码看起来更抽象。
为什么类型名称、导入方式和文档也属于工程设计?
Google 的指南还包含许多乍看之下比较琐碎的规则。
例如,它建议默认导入类型并直接使用类型名,而不是在每次使用时都写完整模块路径。
use regex::Regex; fn main() { let pattern = Regex::new(r"^[a-z]+$").unwrap(); println!("{}", pattern.is_match("rust")); } <...



