Skip to content
Go back

《Software Engineering at Google》这本书教会了我什么

拿到《Software Engineering at Google》的时候,我以为又是一本大厂秀肌肉的书,讲的那套实践大概只有拥有十亿用户和三万工程师的公司才用得上。结果我错了。书里的经验适用于 5 人团队,也适用于 5000 人的组织。这本书来自一个管理着超过 20 亿行代码、每周变更 2500 万行的组织,积累了整整二十年。

这本书不是在教编程。它讲的是 Google 多年来用于维护一个健康代码库的工程实践。更准确地说,它关注的是写完代码之后的事情:你怎么演进它,怎么共享它,怎么测试它,以及最终怎么删掉它。

软件工程 ≠ 编程

这是整本书的核心观点。Titus Winters 在第一章就说清楚了,这个区分也彻底改变了我对自己工作的理解。我们日常把”编程”和”工程”混着用,但这两件事真的不一样。

编程(Programming)是产出代码。你拿到一个任务,写代码解决问题,测试通过,发布上线,然后去做下一件事。

软件工程(Software Engineering)是编程之前和之后发生的一切。你开始对着那段代码提问:我们为什么要做这件事?这对用户有什么影响?需求变化时这段代码怎么演进?它在技术和组织层面怎么扩展?

Titus Winters 有一句精辟的话:“软件工程是编程对时间的积分。” 这句话的分量很重。每一行代码都有生命周期,而工程师的职责是考虑这个生命周期的全部成本,而不只是写代码那个最有趣的部分。

一个黑色星期五活动页面的临时脚本?那是编程。一个未来十年要处理数百万笔交易的支付系统?那需要工程。

Software Engineering at Google 书籍封面

Software Engineering at Google(策划:Titus Winters, Tom Manshreck & Hyrum Wright)

Hyrum 定律和 Beyoncé 规则

这两个概念是我在书中最喜欢的部分,它们总是成对出现,背后有充分的理由。

Hyrum 定律

这大概是全书被引用最多的概念。以 Hyrum Wright(本书策划之一)的名字命名,它的表述是:

当一个 API 的用户数量足够多时,你在契约中承诺了什么已经不重要了:系统所有可观察到的行为都会被某些人依赖。

听起来很理论化,直到它咬你一口。经典案例是哈希迭代顺序。Java 的 HashMap API 明确声明不保证任何顺序,但当 Google 试图升级 Java 版本(导致哈希顺序变化)时,大量测试失败了。工程师们在测试中对 HashMap 的输出使用了 containsElementInOrder() 断言,有人甚至用哈希迭代顺序当低效的随机数生成器。Google 的解决方案是防御性随机化:他们修改了 JDK,让哈希迭代顺序每次运行都不同,从根本上杜绝了对未定义行为的依赖。Python 和 Go 后来也独立做了同样的事。这里的经验是:指责工程师解决不了问题,让犯错变得不可能才行。

Hyrum 定律不只在 Google 的圈子里生效。2024 年,Recall.ai 发布了一个变更,在 S3 URL 路径前加了一个冒号。冒号在 URL 中完全合法,但客户的应用挂了,原因是一个传递性依赖(yarl 库)会自动对 URL 中的”安全”字符做 URL 解码,导致 S3 签名失效。没有人直接解析这些 URL,三层之下的一个依赖建立了一个隐式契约。

Hyrum 定律示意图

Hyrum 定律

Hyrum 定律本质上是软件版本的熵增。你可以缓解它,但永远无法完全消除它。用户越多,你的隐式接口(那些没人写文档但所有人都依赖的行为)就和显式接口一起增长。这就是向后兼容如此困难、破坏性变更在规模化场景下代价如此高昂的原因。

Beyoncé 规则

这条规则很直白:如果你在乎某个行为,就该为它写测试。

假设你修了一个小 bug,没什么大不了的。但账单团队的 Joe 一直依赖那个 bug 让他的代码正常工作,现在 Joe 的东西崩了。谁的错?

Beyoncé 规则说:如果 Joe 在乎那个行为,他就应该写测试。当你的修复破坏了他的测试时,你会立刻看到:“对,得顺便修一下 Joe 的代码。” 因为 Joe 测试了所有他在意的东西,你不需要理解他那堆复杂的业务就能修好它。

我们在自己的项目中也引入了这个做法:当收到带修复方案的 bug 报告时,我们先写测试来证明 bug 被修复了,测试方法的注释里包含 Jira 工单号,方便追溯。

Beyoncé 规则示意图

Beyoncé 规则

这背后更深层的道理是关于共享所有权。在一个大型代码库中,任何人都可能改动你的代码。测试成为一种沟通机制,它们告诉整个组织哪些行为对你来说是重要的。没有测试,你就只能依赖口口相传的部落知识,而那玩意儿没法扩展。

左移(Shift Left)

这个概念很简单但很有力:发现问题越早,修复成本越低。

Google Web Server(GWS),也就是 Google 搜索引擎的基础设施,在 2005 年处于危机状态。超过 80% 的生产部署导致了影响用户的 bug,不得不回滚。技术负责人要求工程师强制自动化测试新代码。一年内,尽管新变更量创了纪录,bug 数减少了一半。今天的 GWS 有数万个测试,每天发布,几乎看不到客户可见的故障。

这清楚地解释了 Google 的测试哲学。书中推崇一个非常精简的测试金字塔:

Google 版测试金字塔

Google 版 Mike Cohn 测试金字塔(来源:Software Engineering at Google

更关键的是,Google 的测试按大小(资源消耗)分类,而不是传统的单元/集成分类。小型测试在单进程、单线程中运行,不做任何 I/O、网络或磁盘访问。中型测试可以在 localhost 上使用多进程。大型测试可以跨机器运行。被优化的核心特性是速度和确定性。

不稳定测试(Flaky tests)得到了特别关注。即使在 Google 0.15% 的不稳定率下,那么大的单仓(monorepo)每天仍然产生数千个不稳定的测试。书中指出,当不稳定率达到约 1% 时,测试就彻底失去意义了,工程师不再关心测试结果,开始无视失败。

开发者工作流时间线

开发者工作流时间线(来源:Software Engineering at Google

三项文化举措让测试在 Google 内部全面铺开。新员工入职培训将测试作为最佳实践来介绍,两年内,受过测试培训的工程师人数超过了测试文化出现之前的工程师人数:

Test Certified 是一个五级双年度计划,通过公开仪表盘产生的社交压力,推动了 1500 多个项目采纳测试流程。

Testing on the Toilet 是从 2006 年 4 月开始贴在厕所隔间里的一页纸测试建议。作者们把它形容为”所有测试倡议中持续时间最长、影响最深远的一项。”

Google 的 Testing on the Toilet 实践

Google 的 Testing on the Toilet 实践(来源:Mike Bland

左移在实践中意味着在开发过程中加入多个质量关卡:

静态分析发生在编辑器里,实时发现拼写错误、错误的函数调用和类型不匹配。修复成本:秒级。

单元测试运行几秒钟,验证代码是否按你的预期工作。修复成本:分钟级。

集成测试运行几分钟,验证系统组件能否协同工作,捕获单元测试遗漏的边界情况。修复成本:一小时左右。

代码评审需要几小时,回答的问题是:这符合团队规范吗?方案合理吗?这也是团队知识共享最好的机制之一。修复成本:大约半天。

QA 需要数小时到数天。所有东西放在一起运行正常吗?修复成本:天级,甚至一周。

生产环境中的用户会发现你遗漏的一切,暴露你从未想象过的边界情况。修复成本:可能巨大,技术上和声誉上都是。

越往右走,修复代价越高。这就是为什么 Google 在静态分析工具(如 Tricorder)、快速单元测试基础设施和预提交检查上投入如此之大。目标是在人类介入之前尽可能多地发现问题。

Critique 的 diff 视图,灰色部分是 Tricorder 的静态分析警告

Critique 的 diff 视图,灰色部分是 Tricorder 的静态分析警告(来源:Software Engineering at Google

别用 Mock 框架

书中第 13 章关于测试替身(Test Doubles)的建议最出人意料:尽可能使用真实实现而非假对象(Fake)和存根(Stub),Mock 只作为最后手段。当 Mock 框架刚进入 Google 时,它们”看上去是一把万能锤子”。写出高度聚焦的测试确实很容易,但代价后来才显现:测试变成了”需要持续维护却很少能发现 bug 的东西”。钟摆现在已经摆到了另一边,很多 Google 工程师选择完全不使用 Mock 框架。

Mock 的根本问题在于它测试的是事情怎么做的,而不是实际发生了什么。一个支付处理器的 Mock 测试可能会检查 pay() 方法是否被用正确的参数调用了,但它无法告诉你付款是否真的成功了。Fake 则是一个跟踪状态的轻量实现,你可以调用 processPayment() 然后检查 getMostRecentCharge() 来看实际发生了什么。Google 甚至在他们的 ErrorProne 静态分析工具中加入了 @DoNotMock 注解。当一个 API 在整个代码库中被 Mock 了数万次,它会”严重限制 API 所有者做出变更的能力”,因为 Mock 在测试中违反了 API 契约,而真实实现和 Fake 根本做不到这一点。

下面是一个 Mock 的例子:

Mock 测试示例

同样的测试用 Fake 来写:

Fake 测试示例

想象一下这个 Mock 在代码库中重复了一万次。每次重构只要改变了 pay() 的实现方式就会全部挂掉,即使支付功能仍然完好无损。

权衡在于 Fake 需要投入来构建。拥有真实实现的团队应该维护对应的 Fake,用契约测试同时验证两者。如果消费者只有几个,写 Fake 可能不值得。但对于热门 API,生产力提升是巨大的。

Mock、Stub 和 Fake 对比

Mock vs Stub vs Fake

代码评审不是 Bug 过滤器

大多数团队把代码评审当质量关卡用。有人检查你的逻辑,点下批准,变更就发了。Google 不是这么玩的,这个差距比你想的要大。

书中说得很直白:检查代码正确性并非 Google 从代码评审中获得的首要收益。更大的回报在于理解力、知识共享和长期可维护性。一个能通过但没人能看懂的变更本身就是问题。看不懂的代码改不了,改不了的代码维护不了。

评审流程比大多数团队想象的要正式:一个同事的 LGTM(这段代码正确且可理解吗?),一个代码所有者的签字(这段代码适合代码库的这个部分吗?),一个可读性批准(它遵循语言标准吗?)。一个人可以同时担任三个角色,实际上也经常如此。但拆分这些角色很重要,每个角色审视的是不同的方面,没有一个审查者需要一次性搞定所有事情。

代码评审流程

代码评审流程(来源:Software Engineering at Google

让整个系统正常运转的两个关键:变更保持在 200 行以内,反馈在 24 个工作小时内给出。Google 三分之一的变更只涉及一个文件。小变更会被仔细阅读,大变更只会被扫一眼。人类天性如此,Google 设计了一个顺应而非对抗这个天性的系统。

最让我记住的一句话是:“如果你从零开始写,你就做错了。” 代码是负债。每一行代码都是未来的维护负担。代码评审是这个现实得到执行的地方,不是作为惩罚,而是一个刚好让你慢下来的机制,让你问自己:这段代码需要存在吗?别人能看懂吗?两年后它还能撑住吗?

Google Critique 工具

Google Critique 工具(来源:Software Engineering at Google

在人的层面上,书中很明确:“你不是你的代码。” 对自己创造的东西产生归属感是自然的。但代码一提交评审,它就不再只属于你了。评审中的每条评论都是一件需要做的事,不是对代码的批评。即使你不同意某条评论,也要解释原因并请对方再看一遍。

书末列出了五个值得贴在墙上的实践:

保持礼貌和专业。 如果觉得有问题,先问,不要假设就是错误。你可能缺少上下文。如果你是作者,把每条评论当 TODO,而不是裁决。

写小变更。 200 行的 diff 会被认真读,2000 行的 diff 只会被批准。把变更聚焦在单个问题上,你得到的反馈才真正有用。

写好变更描述。 描述的第一行会出现在邮件标题、代码搜索结果和未来的调试会话中。“Fix bug” 什么都没说。用一句话解释改了什么以及为什么改。

审查者要少。 一个审查者几乎总是够了。审查者越多,周转越慢,责任越分散。需要第二个意见的话,明确提出。

能自动化的都自动化。 静态分析、格式化、测试覆盖率,机器能抓住的东西不应该浪费人类审查者的注意力。把注意力留给真正需要判断力的决策。

小而频繁的发布

小发布更容易管理、理解和回滚。道理就这么简单。但令人惊讶的是,仍然有大量团队把好几周的工作打包成一个巨型部署,然后花接下来三天调查哪里出了问题。

逻辑很直白:当生产环境出问题时,一个小发布能让你一眼看出是哪个变更造成的。一个包含 50 个提交的巨型发布?祝你好运。

Google 在公司范围内用每秒提交数来追踪速度。为此他们投入了大量 CI/CD 基础设施,让部署小变更变得容易。Feature flags 让他们把”部署代码”和”开启功能”分离开来,代码可以持续部署,即使功能还没准备好面向用户。

DORA(DevOps Research and Assessment)的研究用数据证实了这一点:执行持续交付、每天或每小时发布的团队,在速度、质量、稳定性和开发者满意度方面全面领先。

DORA 指标

DORA 指标

Titus Winters 明确引用了 DORA 的研究,称其提供了因果性而非仅仅相关性的证据,证明基于主干的开发和持续交付能带来更好的技术结果。

尽早、快速、频繁地升级依赖

和发布的道理一样,只不过针对的是你的第三方代码。

从 4.5.8 升到 4.5.9 没什么大不了的。一个补丁版本更新,跑一遍测试就完事了。从 4.5.8 升到 4.8.0 可能需要一些改动,处理几个弃用的 API 和配置变更,但一个下午搞得定。

但从 4.5.8 升到 7.0.0?那就遭罪了。即使所有中间版本都是向后兼容的,差距本身已经大到令人崩溃。隐式接口(Hyrum 定律又来了)变化太多,升级本身已经是一个独立项目了。

书中说得很清楚:“软件工程中最难的未解决问题是依赖管理。” Google 的单仓系统在这方面帮了大忙,但对于我们其他人来说,最好的方案很简单:尽可能保持依赖最新。更小、更频繁的更新永远比更大、更少的迁移便宜。

书中一个很有力的洞察:应该由专家来做更新,而不是消费者。如果你弃用了一个函数,不要只是跟所有人说”请升级”,他们不会动的。你应该去他们的代码里自己做更新。你是最了解该改什么的人,你做得最快。其他人都得切换上下文、翻阅迁移指南、在本轮迭代中挤时间……永远不会发生。

度量生产力是必须的

第 11 章值得单独拎出来说,因为它引入了 Goals/Signals/Metrics(GSM)框架。这是整本书中最能立刻投入使用的工具之一。

在度量任何东西之前,Google 的研究团队会问一系列筛选问题:这值得度量吗?结果会改变某个决策吗?决策者信任我们将产出的数据类型吗?如果任何一个回答是”否”,就不浪费精力。在一个迷恋指标的行业里,光是这一点就够激进了。

GSM 框架

当度量确实值得做时,他们用三个层次来组织:

目标(Goal):期望的最终结果,表述中不引用任何具体指标。(“工程师写出更高质量的代码。”)

信号(Signal):你怎么知道目标达成了,你想度量什么,即使你无法直接度量。(“工程师反馈他们从这个过程中学到了东西。”)

指标(Metric):信号的可测量代理,你实际上度量的东西。(“报告自己在四个相关主题上有所收获的工程师比例,通过调查获取。”)

Goals-Signals-Metrics 流程

Goals-Signals-Metrics 流程

GSM 的威力在于它防止了”路灯效应”(Streetlight Effect),也就是度量容易度量的东西而不是真正重要的东西。从目标出发往下推导,你能确保每个指标都追溯到有意义的目的。

QUANTS:五个维度

Google 的研究团队把生产力分成五个维度,确保改善一个维度时不会悄悄损害另一个:

维度关注的问题
Quality(质量)产出的代码质量如何?
Attention(注意力)工程师能否进入心流状态?是否分心?
iNtellectual complexity(认知复杂度)任务需要多少认知负荷?
Tempo(节奏)工程师完成工作的速度如何?
Satisfaction(满意度)工程师对工具、产品和工作的满意程度?

书中最尖锐的警告涉及个人度量:“如果生产力指标被用于绩效评估,工程师会迅速博弈指标,指标就失去了意义。” 唯一让这些度量有效的办法是度量聚合效果,永远不度量个人。Google 的生产力研究团队专门配备了行为经济学家,用来理解激励结构并防止 Goodhart 定律腐蚀数据。

一个出人意料的发现:“在 Google,我们的经验一再表明,当定量数据和定性数据不一致时,原因是定量指标没有捕捉到预期结果。”

关键收获:如果一个度量不能驱动行动,它就不值得做。每次研究结束后,Google 的团队都会准备一份具体的建议清单:一个新的工具功能、一项文档改进、一次流程变更。如果结果无法推动行动,那这次度量就是白费的。

文化篇:没人想谈但必须谈的事

书的开头几章讲的是文化:团队合作、知识共享、心理安全感和领导力。正如 Titus Winters 在他的 GOTO 演讲中所说,软件工程归根结底只关乎两件事:时间和人。我们教工程师独立写代码,但一旦他们加入团队,这就变成了一项团队运动。

有几个观点特别有共鸣:

天才神话有毒。 真正的成功来自团队协作,而非一个 10x 开发者。天才神话不过是不安全感的另一种表现。书中引用了 Project Aristotle,Google 著名的研究项目,发现心理安全感是高效团队最关键的要素。

“因为我说了算”是领导力的失败。 有分歧的时候,解释你的推理。通过传授来引导人们改变决策,而不是靠权威。作为领导者,我们手里一直有权力和权威,但不应该强制行使。根据我的经验,有能力的人更愿意跟引导型的领导者(仆人式领导者)共事,而不是独裁者。

问”蠢”问题。 Titus 描述过他在主持 C++ 标准子委员会时,会故意问看似天真的问题,确保房间里的每个人在投票前都真正重新理解了材料。展示”不知道也没关系”能创造一种人们真正学习的文化。

把任务委派给能胜任的最初级的人(配合适当的监督)。这直接对抗了 Fred Brooks 的”外科手术团队”模型。团队因此成长,高级工程师被释放出来去攻克下一个不可能的问题。

大多数团队的失败原因不是代码烂,而是缺乏信任。任何团队冲突往回追溯得足够远,你都会发现三样东西中有一样出了裂缝:谦逊(你觉得你的方式是唯一正确的方式)、尊重(你不再关心这个人,只关心工作),或者信任(你宁愿自己做也不愿让别人来驱动)。

从错误中学习,用文档记录下来。 出了事故就写事后复盘(Postmortem)。复盘不是追责,而是趁记忆还热乎的时候记录真相。里面应该包含完整的时间线、真正的根因(不是”人为错误”这类笼统说法),最后以有具体负责人和具体截止日期的行动项结尾。大多数团队在经验教训部分敷衍了事,这是个问题,因为正是这部分能让下一次事故更短。

把这些都做到位了,才能打下坚实的基础,让团队在困难时期能创造价值而不失控。

作者们自己承认的 Google 做不好的事

Titus Winters 对这本书的局限性相当坦诚。在一次 GOTO 播客采访中,他说依赖管理那一章让他”做噩梦”:有很多关于什么行不通的想法,但没有清晰、便宜、可扩展的解决方案。他的指导原则是:“比起一个依赖管理问题,我宁愿面对任意数量的版本控制问题。”

书中对语义化版本(Semantic Versioning)的批评很尖刻:SemVer 本质上是一个估计值,而不是兼容性的证明。一个因为函数 Bar 的破坏性变更而发布的主版本升级,会错误地阻止那些只使用函数 Foo 的消费者升级。而补丁版本虽然理论上安全,实际上经常违反 Hyrum 定律。

语义化版本

语义化版本

Winters 也承认文化章节”有一点理想化”,并不完全描述 Google 的真实状况。批评者指出了一个讽刺的矛盾:Google 一面建议”不要在没有长期支持计划的情况下发布产品”,一面维护着一个臭名昭著的产品坟场

Google 产品坟场

Google Graveyard

另一个真正的弱点是,书中很少为其主张提供超越”在 Google 管用”之外的实证依据。如果能补上这一点,这本书的价值会大得多。

你明天就能用上的实践

Google 的做法不是每一条都适用于每个组织。作者们自己也说了:不要盲目复制 Google 的做法。理解方法背后的”为什么”,然后把”做什么”适配到你的环境。

可以普遍适用的几条:

这本书可以在 abseil.io/resources/swe-book 免费阅读,对于任何软件工程师或技术负责人来说,都是时间投入回报最高的读物之一。

附录:Goodreads 上的 Top 100 软件工程书籍

Dr Milan Milanović 分析了 Goodreads 上排名前 100 的软件工程书籍,有几个有意思的发现:

读得最多的书不是评分最高的。《The Phoenix Project》有 49000 多个评分,得分 4.26。《Clean Code》有 23000 多个评分,得分 4.36。而《Designing Data-Intensive Applications》以仅 10000 个评分拿到了 4.7 的高分,超过了前两者。

老书依然坚挺。K&R 的《The C Programming Language》(1978 年)评分 4.44。《Structure and Interpretation of Computer Programs》(1984 年)评分 4.47。经典之作不是怀旧,它们仍然是标杆。

系统设计内容在快速崛起。Alex Xu 的两本系统设计面试书都进了前 15。五年前,这些书还不存在。

列表底部也很有说明力。《Effective DevOps》的评分只有 3.41。一些架构模式书徘徊在 3.7 附近。期望高,执行参差。

Goodreads Top 100 软件工程书籍评分

Top 100 软件工程书籍,Goodreads 评分排名

完整清单在这里


Tags


Next

2026 年 AI 五大趋势:推理、Agent、编程、开源模型与多模态