聚焦核心原则,挑取最让我眼前一亮的实践点,特别是那些能直接启发或解决我当前工作中痛点的部分。
0. 序言
最近集中精力速读了关于 Google 软件工程实践 的诸多资料(包括官方出版物、工程博客、技术演讲以及社区讨论)。面对 Google 庞大且成熟的工程体系,想要一口吃成胖子显然不现实。因此,我的策略是:聚焦核心原则,挑取最让我眼前一亮的实践点,特别是那些能直接启发或解决我当前工作中痛点的部分。
这份笔记并非对 Google 实践的完整复刻或权威解读,而是我个人学习旅程的记录。它包含:
- 单元测试(Unit Tests)
- 大型测试(Larger Tests)
- 软性可持续性(Sustainability For Software)
- 持续集成,持续交付(CICD)
- 领导力 (Lead a Team)
1. 单元测试
1.1 测试金字塔
谷歌的测试策略基于规模(Size)与范围(Scope)两个维度:
- 规模:
- 小型测试(单进程):限制 I/O 与网络,强制隔离依赖(如内存数据库)。
- 中型测试(单机器):允许跨进程调用(如本地数据库),但禁止跨机器通信。
- 大型测试(多机器):用于端到端验证,但需控制资源消耗。
- 范围:
- 单元测试(80%)→ 集成测试(15%)→ 端到端测试(5%),形成测试金字塔。
对小测试的其他重要限制是,它们不允许休眠,执行 I/O 操作,或进行任何其他阻塞调用。这意味着,小测试不允许访问网络或磁盘。用轻量级的进程内依赖取代重量级依赖。
中型测试可以跨越多个进程并使用线程,并可以对本地主机进行阻塞调用,包括网络调用。剩下的唯一限制是,中型测试不允许对 localhost 以外的任何系统进行网络调用。换句话说,测试必须包含在一台机器内。。例如,你可以运行一个数据库实例来验证你正在测试的代码是否正确地集成在更现实的设置中。或者你可以测试网络用户界面和服务器代码的组合。网络应用程序的测试经常涉及到像 WebDriver 这样的工具,它可以启动一个真正的浏览器,并通过测试过程远程控制它。
大型测试取消了中型测试的本地主机限制,允许测试和被测系统跨越多台机器。例如,测试可能针对远程集群中的系统运行。
为什么 80% 单元测试?
单元测试执行快(毫秒级)、定位准(失败即精准定位问题),是开发流程的“安全网”。但需警惕:仅靠单元测试无法捕获跨组件交互问题,需金字塔上层补充。
1.2 编写“不变”的测试
理想情况下,测试应在需求不变时永不修改。这里有一些最佳实践如下:
- 通过公共 API 测试,而非实现细节
避免因重构(如方法重命名)导致测试失败,聚焦行为契约。
// 错误:测试私有方法
@Test
public void saveToDatabase() { processor.saveToDatabase(transaction); // 私有方法! assertThat(database.get(123)).contains(「me,you,100」);
} // 正确:通过公共行为验证
@Test
public void createUser() { processor.createUser(「Alice」); assertThat(processor.getUser(「Alice」)).isNotNull(); // 公共 API 调用
}
- 编写测试准则
如果一个方法或类的存在只是为了支持一两个其他的类(即,它是一个 「辅助类」),它可能不应该被认为是独立的单元,它的功能测试应该通过这些类进行,而不是直接测试它。
如果一个包或类被设计成任何人都可以访问,而不需要咨询其所有者,那么它几乎肯定构成了一个应该直接测试的单元,它的测试以用户的方式访问该单元。
如果一个包或类只能由其拥有者访问,但它的设计目的是提供在各种上下文中有用的通用功能(即,它是一个“支持库”),也应将其视为一个单元并直接进行测试。这通常会在测试中产生一些冗余,因为支持库的代码会被它自己的测试和用户的测试所覆盖。然而,这种冗余可能是有价值的:如果没有它,如果库的一个用户(和它的测试)被删除,测试覆盖率就会出现缺口。
- 测试行为(Behavior),而非方法(Method)
单测单责,减少“改一行代码,坏十个测试”的连锁反应。
// 错误:单一测试覆盖多行为
@Test
public void testDisplay() { ui.showMessage(「Hello」); ui.showWarning(「Low balance!」); assertThat(ui.getText()).contains(「Hello」, 「Low balance」);
} // 正确:拆分独立行为
@Test
public void showWelcomeMessage() { ... }
@Test
public void showLowBalanceWarning() { ... }
- DAMP > DRY:宁冗余,勿晦涩
测试代码可适度重复以提升可读性
@Test
public void shouldAllowMultipleUsers() {User user1 = newUser().setState(State.NORMAL).build(); //适当冗余User user2 = newUser().setState(State.NORMAL).build();Forum forum = new Forum();forum.register(user1);forum.register(user2);assertThat(forum.hasRegisteredUser(user1)).isTrue();assertThat(forum.hasRegisteredUser(user2)).isTrue();
}@Test
public void shouldNotRegisterBannedUsers() {User user = newUser().setState(State.BANNED).build(); //适当冗余Forum forum = new Forum();try {forum.register(user);} catch(BannedUserException ignored) {}assertThat(forum.hasRegisteredUser(user)).isFalse();
}
}
- 时刻考虑编写可测试代码
数据库、支付网关等重量级依赖时,当无法使用真实实现时,我们要使用依赖注入的方式,简而言之,它需要使用的任何类(即该类的依赖)被传递给它,而不是直接实例化,从而使这些依赖项可以在测试中被替换。
调用这个构造函数的代码负责创建一个合适的 CreditCardService 实例。生产代码可以传入一个与外部服务器通信的 CreditCardService 的实现,而测试可以传入一个测试用的替代
class PaymentProcessor {private CreditCardService creditCardService;PaymentProcessor(CreditCardService creditCardService) {this.creditCardService = creditCardService;}...
}PaymentProcessor paymentProcessor =new PaymentProcessor(new TestDoubleCreditCardService()); // 传入 Mock 的支付网关
- 以被测试的行为命名测试
测试的名字应该概括它所测试的行为。一个好的名字既能描述在系统上采取的行动,又能描述预期的结果。测试名称有时会包括额外的信息,如系统或其环境的状态。一些语言和框架允许测试相互嵌套,并使用字符串命名
multiplyingTwoPositiveNumbersShouldReturnAPositiveNumber
multiply_postiveAndNegative_returnsNegative
divide_byZero_throwsException
- given/when/then
将测试视为与行为而非方法相耦合会显著影响测试的结构。请记住,每个行为都有三个部分:一个是定义系统如何设置的 「given」组件,一个是定义对系统采取的行动的 「when」组件,以及一个验证结果的 「then」组件。当此结构是显式的时,测试是最清晰的。
@Test
public void transferFundsShouldMoveMoneyBetweenAccounts() {// Given two accounts with initial balances of $150 and $20Account account1 = newAccountWithBalance(usd(150));Account account2 = newAccountWithBalance(usd(20));// When transferring $100 from the first to the second accountbank.transferFunds(account1, account2, usd(100));// Then the new account balances should reflect the transfer assertThat(account1.getBalance()).isEqualTo(usd(50));assertThat(account2.getBalance()).isEqualTo(usd(120));
}
- 给出清晰的失败信息
清晰的最后一个方面不是关于测试如何编写的,而是关于测试失败时工程师看到的内容。在一个理想的世界里,工程师可以通过阅读日志或报告中的失败信息来诊断一个问题,而不需要看测试本身。
// 这就是很糟糕的日志提示
Test failed: account is closed// 这个就还可以
Expected an account in state CLOSED, but got account:
<{name: 「my-account」, state: 「OPEN」}
- 共享辅助方法,而不是使用共享变量
许多测试的结构是通过定义一组测试使用的共享值,然后通过定义测试来涵盖这些值如何交互的各种情况。
private static final Account ACCOUNT_1 = Account.newBuilder().setState(AccountState.OPEN).setBalance(50).build();private static final Account ACCOUNT_2 = Account.newBuilder().setState(AccountState.CLOSED).setBalance(0).build();private static final Item ITEM = Item.newBuilder().setName(「Cheeseburger」).setPrice(100).build();// Hundreds of lines of other tests...
此策略可以使测试非常简洁,但随着测试套件的增长,它会导致问题。首先,很难理解为什么选择某个特定值进行测试。幸运的是,测试名称澄清了正在测试的场景,但你仍然需要向上滚动到定义,以确认 ACCOUNT_1 和 ACCOUNT_2 适用于这些场景。
工程师通常倾向于使用共享常量,因为在每个测试中构造单独的值可能会很冗长。实现此目标的更好方法是使用辅助方法构造数据,该方法要求测试作者仅指定他们关心的值,并为所有其他值设置合理的默认值。在支持命名参数的语言中,这种构造非常简单:
# A helper method wraps a constructor by defining arbitrary defaults for
# each of its parameters.
def newContact(firstName = 「Grace」, lastName = 「Hopper」, phoneNumber = 「555-123-4567」):return Contact(firstName, lastName, phoneNumber)# Tests call the helper, specifying values for only the parameters that they
# care about.
def test_fullNameShouldCombineFirstAndLastNames(self):def contact = newContact(firstName = 「Ada」, lastName = 「Lovelace」) self.assertEqual(contact.fullName(), 「Ada Lovelace」)// Languages like Java that don’t support named parameters can emulate them
// by returning a mutable 「builder」 object that represents the value under
// construction.
private static Contact.Builder newContact() {return Contact.newBuilder().setFirstName(「Grace」).setLastName(「Hopper」).setPhoneNumber(「555-123-4567」);
}// Tests then call methods on the builder to overwrite only the parameters
// that they care about, then call build() to get a real value out of the
// builder. @Test
public void fullNameShouldCombineFirstAndLastNames() {Contact contact = newContact().setFirstName(「Ada」).setLastName(「Lovelace」).build();assertThat(contact.getFullName()).isEqualTo(「Ada Lovelace」);
}
- 不要在测试中放入逻辑
清晰的测试在检查时通常是正确的;也就是说,很明显,只要看一眼,测试就做了正确的事情。这在测试代码中是可能的,因为每个测试只需要处理一组特定的输入,而产品代码必须被泛化以处理任何输入。对于产品代码,我们能够编写测试,确保复杂的逻辑是正确的。但测试代码没有那么奢侈——如果你觉得你需要写一个测试来验证你的测试,那就说明出了问题!这是不可能的。
复杂性最常以逻辑的形式引入。逻辑是通过编程语言的指令部分来定义的,如运算符、循环和条件。当一段代码包含逻辑时,你需要做一些心理预期来确定其结果,而不是仅仅从屏幕上读出来。不需要太多的逻辑就可以使一个测试变得更难理解。
//掩盖 bug 的逻辑
@Test
public void shouldNavigateToAlbumsPage() {String baseUrl = 「HTTP://photos.google.com/」;Navigator nav = new Navigator(baseUrl);nav.goToAlbumPage();assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + 「/albums」);
}//没有逻辑的测试揭示了 bug
@Test
public void shouldNavigateToPhotosPage() {Navigator nav = new Navigator(「HTTP://photos.google.com/」);nav.goToPhotosPage();assertThat(nav.getCurrentUrl())).isEqualTo(「HTTP://photos.google.com//albums」); // Oops! 多了一个/
}
- 推荐状态测试而非交互测试
在谷歌,我们发现强调状态测试更具可扩展性;它降低了测试的脆弱性,使得随着时间的推移更容易变更和维护代码。通过状态测试,你可以调用被测系统,并验证返回的值是否正确,或者被测系统中的其他状态是否已正确更改。
@Test
public void sortNumbers() {NumberSorter numberSorter = new NumberSorter(quicksort, bubbleSort);// Call the system under test.List sortedList = numberSorter.sortNumbers(newList(3, 1, 2));// Validate that the returned list is sorted. It doesn’t matter which// sorting algorithm is used, as long as the right result was returned.assertThat(sortedList).isEqualTo(newList(1, 2, 3));
}// 说明了一个类似的测试场景,但使用了交互测试。请注意,此测试无法确定数字是否实际已排序,
// 因为测试替代不知道如何对数字进行排序——它所能告诉你的是,被测试系统尝试对数字进行排序。
@Test
public void sortNumbers_quicksortIsUsed() {// Pass in test doubles that were created by a mocking framework.NumberSorter numberSorter = new NumberSorter(mockQuicksort, mockBubbleSort);// Call the system under test.numberSorter.sortNumbers(newList(3, 1, 2));// Validate that numberSorter.sortNumbers() used quicksort. The test// will fail if mockQuicksort.sort() is never called (e.g., if// mockBubbleSort is used) or if it’s called with the wrong arguments.verify(mockQuicksort).sort(newList(3, 1, 2));
}
2. 大型测试
在测试金字塔中,单元测试是基石,但大型测试才是确保系统级可靠性的关键屏障。谷歌通过 20 年实践,构建了一套应对复杂系统挑战的大型测试体系。
谷歌通过三个维度定义测试策略:
维度小型测试大型测试规模单进程/单线程跨进程/跨机器集群范围单个类/模块多服务协同/全链路执行时间<1 秒 15
维度 | 小型测试 | 大型测试 |
---|---|---|
规模 | 单进程/单线程 | 跨进程/跨机器集群 |
范围 | 单个类/模块 | 多服务协同/全链路 |
执行时间 | <1秒 | 15分钟至数天 |
分钟至数天
2.1 大型测试组成
- 获得被测试的系统
# 典型社交广告系统 SUT 拓扑
sut = {「前端服务」: [「Web 服务器」, 「移动端」],「中间层」: [「广告服务」, 「用户画像服务」],「数据层」: [「MySQL」, 「BigTable」, 「索引管道」]
} #
为了解决规模问题,我们通过用内存数据库替换它的数据库,并移除 SUT 范围之外的一个我们真正关心的服务器,使这个 SUT 变得更小,如图 14-6 所示。这个 SUT 更可能适合在一台机器上使用。
- 必要的测试数据
手工制作数据
与小型测试一样,我们可以手动创建大型测试的测试数据。
但是在一个大型 SUT 中为多个服务设置数据可能需要更多的工作,并且我们可能需要为大型测试创建大量数据。复制的数据
我们可以复制数据,通常来自生产。
例如,我们可以通过从生产地图数据的副本开始测试地球地图,以提供基线,然后测试我们对它的更改。抽样数据
复制数据可能产生过多,难以有效处理的数据。
采样数据可以减少数量,从而减少测试时间,使其更容易推理。「智能抽样 」包括复制最小的数据以达到最大覆盖率的技术。
- 验证行为
手动就像你在本地尝试你的二进制文件一样,手动验证使用人工与 SUT 互动以确定它的功能是否正确。
这种验证可以包括通过执行一致的测试计划中定义的操作来测试回归,
也可以是探索性的,通过不同的交互路径来识别可能的新故障。
需要注意的是,人工回归测试的规模化不是线性的:系统越大,通过它的操作越多,需要的人力测试时间就越多就越多。断言与单元测试一样,这些是对系统预期行为的明确检查。例如,对于谷歌搜索 xyzzy 的集成测试,一个断言可能如下:
assertThat(response.Contains(「Colossal Cave」))A/B 测试(差异)A/B 测试不是定义显式断言,而是运行 SUT 的两个副本,发送相同的数据,并比较结果。
未明确定义预期行为:人工必须手动检查差异,以确保任何预期更改。
2.2 Google 大型测试模型
测试模型 | SUT | 数据 | 验证 |
---|---|---|---|
一个或多个二进制文件的功能测试 | 单机密封或云部署隔离 | 手工制作 | 断言 |
性能、负载和压力测试 | 云部署隔离 | 手工生成或从生产中多路传输 | 差异(性能指标) |
部署配置测试 | 单机封闭或云部署隔离 | 无 | 断言(不会崩溃) |
探索性测试 | 生产或共享预发 | 生产或已知测试范围 | 手动 |
A/B对比测试 | 两个云部署的隔离环境 | 通常从生产或取样中多路传输 | A/B差异比较 |
探针和金丝雀分析 | 生产 | 生产 | 断言和A/B差异(度量) |
故障恢复与混沌工程 | 生产 | 生产和用户定制(故障注入) | 手动和A/B对比(指标) |
用户评价 | 生产 | 生产 | 手动和A/B对比(指标) |
- 一个或多个二进制文件的功能测试
一个常见的案例是在微服务环境中,当服务被部署为许多独立的二进制文件。在这种情况下,功能测试可以通过提出由所有相关二进制文件组成的 SUT,并通过发布的 API 与之交互,来覆盖二进制文件之间的真实交互。
- 部署配置测试
很多时候,缺陷的根源不是代码,而是配置:数据文件、数据库、选项定义等等。
这种测试实际上是 SUT 的冒烟测试,不需要太多额外的数据或验证。如果 SUT 成功启动,则测试通过。否则,测试失败。
- 探索性测试
训练有素的用户/测试人员通过产品的公共 API 与产品交互,在系统中寻找新的路径,寻找行为偏离预期或直观行为的路径,或者是否存在安全漏洞。
在某种意义上,这有点像功能集成测试的手动“模糊测试”版本。
- A/B 对比测试
A/B 对比测试通过向公共 API 发送流量并比较新旧版本之间的响应(特别是在迁移期间)。
任何行为上的偏差都必须作为预期的或未预期的(回归)进行调整。
在这种情况下,SUT 由两组真实的二进制文件组成:一个运行在候选版本,另一个运行在基本版本。第三个二进制程序发送流量并比较结果。
- 探针和金丝雀分析
探针和金丝雀分析是确保生产环境本身健康的方法。在这些方面,它们是生产监控的一种形式,但在结构上与其他大型测试非常相似。
Probers 是功能测试,针对生产环境运行编码的断言。通常,这些测试执行众所周知的和确定的只读动作,这样即使生产数据随时间变化,断言也能成立。例如,探针可能在 http://www.google.com 执行谷歌搜索,并验证返回的结果,但实际上并不验证结果的内容。在这方面,它们是生产系统的 「冒烟测试」,但可以及早发现重大问题。
金丝雀分析也是类似的,只不过它关注的是一个版本何时被推送到生产环境。如果发布是分阶段进行的,我们可以同时运行针对升级(金丝雀)服务的探针断言,以及比较生产中金丝雀和基线部分的健康指标,并确保它们没有失衡。
- 故障恢复与混沌工程
多年来,谷歌每年都会举办一场名为“灾难恢复测试”DiRT(Disaster Recovery Testing)的演练,在这场演练中,故障几乎以全球规模注入我们的基础设施。我们模拟了从数据中心火灾到恶意攻击的一切。在一个令人难忘的案例中,我们模拟了一场地震,将我们位于加州山景城的总部与公司其他部门完全隔离。这样做不仅暴露了技术上的缺陷,也揭示了在所有关键决策者都无法联系到的情况下,管理公司的挑战。
DiRT 测试的影响需要整个公司的大量协调;相比之下,混沌工程更像是对你的技术基础设施的 「持续测试」。由 Netflix 推广,混沌工程包括编写程序,在你的系统中不断引入背景水平的故障,并观察会发生什么。有些故障可能相当大,但在大多数情况下,混沌测试工具旨在在事情失控之前恢复功能。混沌工程的目标是帮助团队打破稳定性和可靠性的假设,帮助他们应对建立弹性的挑战。今天,谷歌的团队每周都会使用我们自己开发的名为 Catzilla 的系统进行数千次混沌测试。
- 用户评价
基于产品的测试可以收集大量关于用户行为的数据。我们有几种不同的方法来收集有关即将推出的功能的受欢迎程度和问题的指标,这为我们提供了 UAT 的替代方案:
- 吃自己的狗粮
我们可以利用有限的推广和实验,将生产中的功能提供给一部分用户使用。我们有时会和自己的员工一起这样做(吃自己的狗粮),他们会在真实的部署环境中给我们提供宝贵的反馈。 - 实验
在用户不知情的情况下,将一个新的行为作为一个实验提供给一部分用户。然后,将实验组与控制组在某种期望的指标方面进行综合比较。例如,在 YouTube,我们做了一个有限的实验,改变了视频加分的方式(取消了降分),只有一部分用户看到了这个变化。 这是一个对谷歌来说非常重要的方法。Noogler 在加入公司后听到的第一个故事是关于谷歌推出了一个实验,改变了谷歌搜索中 AdWords 广告的背景阴影颜色,并注意到实验组的用户与对照组相比,广告点击量明显增加。 - 评分员评价
评分员会被告知某一特定操作的结果,并选择哪一个 「更好」以及原因。然后,这种反馈被用来确定一个特定的变更是正面、中性还是负面的。例如,谷歌在历史上一直使用评分员对搜索查询进行评估(我们已经公布了我们给评员者的指导方针)。在某些情况下,来自该评级数据的反馈有助于确定算法更改的启动通过/不通过。评价员的评价对于像机器学习系统这样的非确定性系统至关重要,因为这些系统没有明确的正确答案,只有一个更好或更差的概念。
3. 软性可持续性
在超大规模代码库(20 亿+行代码)和数万名工程师的协作环境下,谷歌形成了一套独特的工程实践体系。其核心在于将规则、审查与文档视为代码健康的支柱,通过流程设计与工具链实现可持续开发。以下分享谷歌的关键实践理念。
3.1 风格指南即“法律”
Google 风格指南 | styleguide
3.2 代码审查
在软件行业,“代码审查”常被视为质量控制手段,但谷歌将其提升为工程文化的核心支柱。通过独特的流程设计和工具支持,谷歌让代码审查成为连接十万工程师的神经网络,驱动着全球最大单体代码库的协作进化。以下是谷歌代码审查体系的精髓:
- 创建一个变更。 用户对其工作区的代码库进行变更。然后这个作者向 Critique 上传一个快照(显示某一特定时间点的补丁),这将触发自动代码分析器的运行(见第 20 章)。
- 要求审查。 在作者对修改的差异和 Critique 中显示的分析器的结果感到满意后,他们将修改发送给一个或多个审查员。
- 评论。审查者在 Critique 中打开变更,并对 diff 起草评论。评论默认标记为未解决,意味着它们对作者来说是至关重要的。此外,评论者可以添加已解决的评论,这些评论是可选的或信息性的。自动代码分析器的结果,如果存在的话,也可以让审查者看到。一旦审查者起草了一组评论,他们需要发布它们,以便作者看到它们;这样做的好处是允许审查者在审查了整个修改后,以原子方式提供一个完整的想法。任何人都可以对变更发表评论,并在他们认为必要时提供“驱动式审查”。
- 修改变更并回复评论。 作者修改变更,根据反馈上传新的快照,并回复评论者。作者处理(至少)所有未解决的评论,要么修改代码,要么直接回复评论并将评论类型改为解决。作者和审稿人可以查看任何一对快照之间的差异,看看有什么变化。步骤 3 和 4 可能要重复多次。
- 变更批准。 当审查者对修改的最新状态感到满意时,他们会批准变更,并将其标记为 “我觉得不错「(LGTM)。他们可以选择包含已解决的评论。更改被认为适合提交后,在 UI 中会清楚地标记为绿色以显示此状态。
- 提交变更。 只要变更被批准(我们很快会讨论),作者就可以触发变更的提交过程。如果自动分析器和其他预提交钩子(称为 「预提交」)没有发现任何问题,该变更就被提交到代码库中。
- 原子化提交
每个变更(Change)必须是独立、可理解的代码单元,平均仅 200 行。小变更加速审查流转,35% 的修改仅涉及单个文件。
- 3bits 认证
LGTM(Looks Good To Me):核心工程师验证代码正确性及可理解性,但要求权限最小化,85% 变更仅需 1 人 LGTM,避免设计委员会式审查
代码所有权(Code Ownership):目录级 OWNERS 文件指定维护者,确保变更符合模块设计哲学
可读性认证(Readability):语言专家确保代码符合谷歌规范
- 自动化先锋
静态检查、单元测试等 60% 机械性工作由 Critique 工具在预提交阶段自动完成,人类聚焦核心逻辑。
- 写好变更描述
变更描述应该在第一行标注它的变更类型,作为一个摘要。第一行是最重要的,它被用来在代码审查工具中提供摘要,作为任何相关电子邮件的主题行,并成为谷歌工程师在代码搜索中看到的历史摘要的可见性。
首行摘要遵循<类型>: <影响域> - <目标>
格式,例如:
perf: memory_cache - reduce latency by 40%
- 代码即负债
谷歌工程师常被提醒:“如果你在重写轮子,那你就错了”。审查流程中贯穿着对代码膨胀的警惕:
新库提交需证明无现有解决方案
每新增 1 万行代码需专项审查
废弃系统下线的审查优先级高于新功能
3.3 文档即代码
在软件工程领域,文档的质量一直是工程师们的集体痛点。谷歌内部调查曾显示,「文档过时、缺失或难以理解」 连续多年位居开发者抱怨榜首位。当工程师面对这些问题时:
- “这个方法有什么副作用?”
- “第三步之后报错了!”
- “这个缩写到底什么意思?”
- “这文档还是最新的吗?”
谷歌最终发现:文档不是附属品,而是工程流程的核心组件。以下是他们的实践精髓:
- 文档 == 代码
版本控制:文档与代码共存于同一仓库,修改需通过代码审查(Code Review)
自动化测试:定期扫描陈旧文档(如添加 freshness
元数据校验最后更新时间)
明确所有权:每个文档必须有负责人(Owner),避免“孤儿文档”
问题追踪:文档 BUG 纳入 Jira 等系统,与代码缺陷同等处理
- 写给谁看?明确区分读者
- 完美主义陷阱
你不需要媲美海明威,只需让另一个‘你’看懂。”
- 文档是时间的盟友
谷歌工程师算过一笔账:
写 1 小时文档 ≈ 省下 50 小时解答重复问题
因为:
⏳ 设计阶段写文档能暴露 API 漏洞(“无法清晰描述的功能必然设计失败”)
文档阅读次数是指数级的(1 次编写 vs 1000+次阅读)
4. 持续集成,持续交付
4.1 代码左移
向左移动。通过 CI 和持续部署,使所有的变化更快,更多的数据驱动的决策更早。
4.2 经常对基础设施进行升级
4.3 早失败、快失败、经常失败
高频发布降低风险
容忍可控缺陷(如菲律宾方言显示问题),但通过渐进式发布控制影响范围。
一个好的事后总结应该包括以下内容:
- 事件的简要概述
- 事件的时间线,从发现、调查到解决的过程
- 事件的主要原因
- 影响和损害评估
- 一套立即解决该问题的行动项目(包括执行人)。
- 一套防止事件再次发生的行动项目
- 经验教训
4.4 静态检查工具
4.5 发布序列
“发布列车准时出发” – 截止时间后拒绝新变更。
自动化构建 + 准隔日发布,发布周期缩短 85%。
4.6 反庸肿策略:按需交付
- 动态交付:仅下发用户所需代码模块,减少 30% 应用体积。
- 成本监控:自动追踪功能使用率,废弃低价值功能。
4.7 功能开关
新功能的代码可以提前部署到生产环境,但通过开关控制其是否对用户可见。这支持了基于主干开发(Trunk-Based Development),减少了分支合并的冲突和延迟。通过渐进式发布(如金丝雀发布、灰度发布)、A/B 测试,可以小范围验证新功能,监控性能和用户反馈,出现问题时能快速关闭功能以实现回滚,无需重新部署代码
5. 领导力
- 坚持“A 级人才雇佣 A 级人才”,宁缺毋滥。
- 放下自负,做服务型的领导,无论是清除官僚障碍、深夜订餐慰劳团队,还是保护团队免受组织动荡干扰。
- 成为团队催化剂,扫除团队无法解决的障碍
- 用单一使命声明对齐方向:“如果团队是拉货车的绳子,确保所有人朝北拉,而非各自用力。”
- 追踪幸福感,在 1:1 会议必问:“What do you need?” 并关注非工作因素
- 不要有成果焦虑,不再有“今日写 500 行代码”的即时成就感,需适应“赋能他人”的长周期价值。
- 谦虚,礼貌,有集体意识
最好的是,考虑以 「集体 」的自我为目标;与其担心你个人是否了不起,不如尝试建立一种团队成就感和团体自豪感。给予建设性批评的人真正关心对方,希望他们提升自己或工作。学会尊重同龄人,礼貌地提出建设性的批评:
“嘿,我对这部分的控制流感到困惑。我想知道 XYZY 代码模式是否能让这更清晰、更容易维护?”记住你是如何用谦逊的方式来回答这个问题,而不是指责。他们没有错;你只是理解代码有点困难。
- 心理安全环境
要学习,你必须首先承认有些事情你不明白。我们应该欢迎这种诚实,而不是惩罚它。心理安全是促进学习环境的关键。在一个健康的团队中,队友们不仅愿意回答问题,也愿意提出问题:表明他们不知道的东西,并相互学习。
- 确保每个责任领域除了一个主要和一个次要所有者,以及可用的文档
记住:团队成员可能不会被公交车撞到,但其他不可预知的事件仍然会发生。有人可能会结婚、搬走、离开公司或请假照顾生病的亲属。确保每个责任领域除了一个主要和一个次要所有者之外,至少还有可用的文档,这有助于确保项目的成功,提高项目的成功率。希望大多数工程师认识到,成为成功项目的一部分比成为失败项目的关键部分要好。
6. 后记
这份笔记的目的,一是固化自己的学习成果,二是抛砖引玉,希望能为同样对构建高效、可持续工程文化感兴趣的同行提供一些参考和讨论的起点。Google 的实践是其特定规模、历史和文化下的产物,但它所蕴含的工程智慧,无疑是值得学习和借鉴的“活水”。
Reference
Software Engineering at Google