聚焦核心原則,挑取最讓我眼前一亮的實踐點,特別是那些能直接啟發或解決我當前工作中痛點的部分。
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