概述#

大多數工程組織都有管理程式碼庫的規則——從原始碼存放位置、程式碼格式,到命名慣例、設計模式與例外處理。在 Google,這些規則被彙整於一套 風格指南(Style Guides) 中,作為所有工程師必須遵守的準則。

規則(Rules) 是法律,具有強制性,不得擅自忽略。相對地,指引(Guidance) 則是建議與最佳實踐,具有彈性空間。

Google 的「風格指南」涵蓋的不僅是格式化實踐,而是管理程式碼的完整慣例集合——包括命名、模式、語言特性使用等所有面向。

Google 為每種使用中的程式語言維護獨立的風格指南。各指南在範圍、長度和內容上差異很大:

  • 簡潔型:如 Dart、R、Shell 指南,聚焦於命名與格式等少數原則
  • 詳盡型:如 C++、Python、Java 指南,深入特定語言特性
  • 外部對齊型:如 Go 指南,僅在外部慣例基礎上添加少量規則
  • 特殊偏離型:如 C++ 指南禁止使用 exceptions,與外部社群慣例不同

為什麼需要規則?#

規則的目標是鼓勵「好的」行為、阻止「壞的」行為。對「好」與「壞」的定義因組織而異:

  • 有些組織重視記憶體效率或執行期優化
  • 有些組織鼓勵使用新語言特性
  • 有些組織最在乎一致性

隨著組織成長,規則與指引塑造出共同的程式語彙(common vocabulary)。這讓工程師能專注於程式碼「要說什麼」,而非「怎麼說」。規則提供了廣泛的槓桿,將常見的開發模式導向期望的方向。

制定規則#

制定規則時,關鍵問題不是「我們應該有哪些規則?」而是「我們想推進什麼目標?」聚焦目標後,辨識哪些規則支持該目標就變得容易許多。

指導原則#

Google 工程組織超過 30,000 名工程師,每天約有 60,000 次提交,程式碼庫超過 20 億行且預計存續數十年。在此背景下,規則的目標是管理開發環境的複雜度,在維持程式碼庫可管理的同時讓工程師保持生產力。

規則開發遵循以下核心原則:

  1. 規則必須值得其代價(Pull their weight)
  2. 為讀者優化(Optimize for the reader)
  3. 保持一致性(Be consistent)
  4. 避免易出錯和令人意外的構造(Avoid error-prone and surprising constructs)
  5. 在必要時向現實妥協(Concede to practicalities when necessary)

規則必須值得其代價#

每條新規則都有非零成本——所有工程師都需要學習和適應。規則過多會導致:

  • 工程師難以記住所有相關規則
  • 新進工程師的學習門檻提高
  • 規則集本身的維護成本增加

Google 刻意不納入被視為理所當然的規則。例如,C++ 風格指南沒有禁止使用 goto 的規則——因為 C++ 工程師本來就不太使用它。如果只有一兩個工程師犯錯,為所有人增加心智負擔並不划算。

衡量規則「是否過多」的指標不是規則的原始數量,而是工程師需要記住多少。自動化工具(如 clang-format)可以大幅降低遵循格式化規則的成本。

為讀者優化#

程式碼被閱讀的頻率遠高於被撰寫的頻率,因此 Google 寧願程式碼打字繁瑣也不願閱讀困難

  • 偏好簡單易讀:Python 風格指南限制條件表達式(conditional expressions)的使用,因為雖然比 if 語句更簡潔,但對讀者更難理解
  • 要求明確的行為證據:Java、JavaScript、C++ 指南要求覆寫方法時必須使用 override 註解或關鍵字
  • 支持本地推理(Local reasoning):讀者不需查閱其他程式碼就能理解當前程式碼片段

C++ 所有權轉移範例:Google 偏好使用 std::unique_ptr 明確表達所有權轉移,而非裸指標:

// 不良:無法從呼叫端看出所有權意圖
void TakeFoo(Foo* arg);
Foo* my_foo(NewFoo());
TakeFoo(my_foo);

// 良好:呼叫端明確顯示所有權轉讓
void TakeFoo(std::unique_ptr<Foo> arg);
std::unique_ptr<Foo> my_foo(FooFactory());
TakeFoo(std::move(my_foo));

註解規則同樣服務於「為讀者提供就地證據」的目標:

  • 文件註解(Documentation comments):描述後續程式碼的設計或意圖
  • 實作註解(Implementation comments):解釋非顯而易見的選擇、技巧性的段落、重要的程式碼片段

保持一致性#

Google 對程式碼庫一致性的看法,類似於對辦公室的態度:各地辦公室保有獨特風格,但必要的工作設施(門禁卡、WiFi、視訊會議介面)刻意保持相同。程式碼也是如此——本地專案可以有獨特個性,但工具、技術和函式庫都保持一致。

一致性的優勢#

  • 提升專注度與理解速度:一致的風格與規範使工程師能專注於程式碼的功能而非呈現方式,支持認知心理學中的專家分塊(expert chunking)——以模式而非個別元素理解程式碼
  • 支持工具化擴展:一致的程式碼結構讓工具可以理解、編輯和生成程式碼。如果不同專案採用不同的 import 排序策略,統一工具就無法全面運作
  • 降低人員流動成本:一致的程式碼庫讓跨專案調動、SRE 跨專案偵錯、函式庫工程師跨專案維護都更加高效
  • 確保時間韌性:隨著時間推移,人員離開、新人加入、所有權轉移——一致的程式碼庫確保這些轉換的成本最低

規模下的妥協:Google C++ 風格指南曾承諾幾乎不會更改使舊程式碼不一致的規則。但隨著程式碼庫增長,要求完美一致的成本已超過其價值。現在 Google 透過 Large Scale Change 工具盡可能更新大部分舊程式碼,但不再追求 100% 的一致性。

設定標準#

一致性遵循層級結構:檔案內部慣例 > 團隊慣例 > 專案慣例 > 整體程式碼庫慣例。

同時也需考慮外部標準。對於短期小型專案,內部一致性更重要;但長期來看,遵循廣泛接受的外部標準通常會帶來回報。

案例:Python 縮排——Google 最初為 Python 使用兩格縮排(與 C++ 一致),但外部 Python 社群使用四格縮排。隨著時間推移,工程師查閱外部程式碼和匯出開源專案的成本不斷增加。最終在 Starlark(Google 設計的建構描述語言)制定風格指南時,改採四格縮排以與外部世界保持一致。

避免易出錯和令人意外的構造#

風格指南限制語言中較令人意外、不常見或具技巧性的構造。原因是:

  • 複雜特性常有不易察覺的陷阱
  • 即使專案工程師理解某構造,未來的維護者不一定具備相同理解
  • SRE 在除錯生產事故時可能需要閱讀不熟悉語言的程式碼

Python 反射範例hasattr()getattr() 允許用字串存取物件屬性,但這會使程式碼難以搜尋、難以驗證、難以測試,甚至引發安全漏洞:

# 不良:使用字串間接存取屬性,來源可能是 RPC 或資料儲存
values = []
for field in some_file.A_CONSTANT:
    values.append(getattr(my_object, field))

Google 更重視簡潔、直觀、易於理解和維護的程式碼,而非僅讓專家能完美使用的進階特性。

向現實妥協#

引用 Ralph Waldo Emerson:「愚蠢的一致性是小心靈的妖怪。」(A foolish consistency is the hobgoblin of little minds.)

在追求一致性的同時,也需承認例外的存在:

  • 效能考量:C++ 風格指南禁止 exceptions,但允許使用 noexcept(可觸發編譯器優化)
  • 互通性:C++ 指南允許模仿標準函式庫特性時使用 snake_case(而非一般要求的 CamelCase);Windows 程式開發允許多重繼承以相容平台特性
  • 生成的程式碼:Java 和 JavaScript 風格指南明確將自動生成的程式碼排除在規則範圍外

風格指南的內容#

風格指南規則大致分為三大類:

避免危險#

包含基於技術原因「必須做」或「不得做」的規則:

  • 靜態成員與變數的使用方式
  • Lambda 表達式的規範
  • 例外處理的規則
  • 多執行緒、存取控制、類別繼承的建構方式
  • 標準詞彙型別(vocabulary types)的指定用途
  • 難以正確使用之特性的明確裁決

每條規則都附帶優缺點分析決策理由,大多基於時間韌性的需求。

執行最佳實踐#

涵蓋維持程式碼庫健康與可維護性的規則:

  • 註解規範:通用註解慣例、特殊情況(如 switch fall-through、空 catch 區塊、模板元程式設計)的文件要求
  • 原始碼結構:檔案組織、內容排列順序
  • 命名規範:套件、類別、函式、變數的命名
  • 格式化規則:垂直與水平空白、行長度限制、大括號對齊
  • 新特性限制:對尚未充分理解的語言特性預設安全圍欄

案例:std::unique_ptr 的引入——C++11 引入 std::unique_ptr 時,Google 風格指南最初禁止使用它,因為 move semantics 對多數工程師而言太新且令人困惑。隨著時間推移,工程師逐漸適應了 move semantics,Google 認識到 std::unique_ptr 在函式呼叫端提供的所有權資訊對讀者的價值巨大,最終修改規則允許使用。

對於新特性,Google 採取漸進式開放策略:

  1. 初期限制使用
  2. 觀察豁免申請中的使用模式
  3. 從實際案例中歸納最佳實踐
  4. 修訂規則以允許更廣泛的使用

建立一致性#

許多規則處理的是技術上無顯著差異的細節——命名慣例、縮排空格數、import 排序等。這類規則的價值不在於「選擇了什麼」,而在於「做出了選擇」,從而終結無盡的辯論循環。

指南之外#

風格指南不包含所有軟體工程建議。許多基本的工程原則——不要耍聰明、不要分叉程式碼庫、不要重新發明輪子——並未明文列入。風格指南假設工程師已具備這些基礎知識。

變更規則#

風格指南不是靜態的。隨著時間推移,多種因素可能觸發規則的重新評估:

  • 新語言版本發布,需要允許或排除新特性
  • 工程師花費大量力氣繞過某規則,需重新審視其效益
  • 執行規則的工具變得過於複雜而難以維護

決策依據#

每條規則背後都有證據支持。Google 在新增規則時會分析優缺點和潛在影響,並記錄推理過程。這些文件化的理由讓團隊能在條件改變時辨識何時需要重新評估。

案例:CamelCase 命名——Google Python 風格指南最初選擇 CamelCase(與 C++ 一致),而非 Python 社群的 snake_case。隨著 Python 專案獨立發展、與外部程式碼互動增加、開源專案的維護成本上升,Google 最終修改規則允許使用 snake_case,但以檔案為單位選擇,且現有程式碼可豁免。

變更流程#

變更流程是基於解決方案的:

  1. 識別既有問題:以實際程式碼中的模式為證據,而非假設性情境
  2. 社群討論:任何工程師都可透過語言專屬的郵件列表提出問題或變更提案
  3. 社群回饋:提案經社群討論,部分被共識否決,部分獲得正面回饋
  4. 最終裁決:通過社群審查的提案提交給風格仲裁者做最終決定

風格仲裁者(Style Arbiters)#

每種語言的風格指南都有指定的風格仲裁者——由資深語言專家組成的決策小組。決策方式有幾個重要特點:

  • 決策基於工程權衡而非個人偏好
  • 在既定目標框架內評估修改的利弊
  • 共識而非投票做出決定(C++ 仲裁組為四人,偶數人數因共識決策而無礙)

例外處理#

規則可以有例外,但豁免不會輕易授予:

  • 合理豁免:如隱式型別轉換規則的豁免——透明包裝型別(transparent wrapper types)可合理允許隱式轉換
  • 不合理豁免:僅因巨集名稱長度或專案偏好而申請的豁免會被駁回,因為程式碼庫的完整性高於專案的一致性

指引(Guidance)#

除了規則之外,Google 還以多種形式提供程式設計指引:

Primers(入門指南)#

針對主要程式語言的描述性指南,解釋風格指南認可的語言特性的使用方式。涵蓋面廣泛,為新加入 Google 特定語言開發的工程師提供參考。

Tip of the Week#

Google 發行一系列 C++ 提示,涵蓋:

  • 困難主題:物件生命週期、copy/move 語義、argument-dependent lookup
  • 新特性:C++11/C++17 特性如 string_viewoptionalvariant
  • 常見錯誤矯正:避免 using 指令、注意隱式 bool 轉換

這些提示短小精悍,每篇僅需數分鐘閱讀,在 code review 和技術討論中被頻繁引用。

Language@Google 101 課程#

針對每種主要程式語言的全天課程,涵蓋:

  • 在 Google 程式碼庫中使用該語言的獨特之處
  • 最常用的函式庫和慣用模式
  • 內部偏好與自訂工具使用

隨手參考資源#

  • 特定語言中較難掌握領域的建議(如並行處理與雜湊)
  • 語言更新引入新特性的詳細分析與使用建議
  • 函式庫提供的關鍵抽象與資料結構清單

執行規則#

規則的價值在於可執行性。Google 透過社會性技術性兩種途徑來執行規則。

社會性執行#

  • 正式培訓課程:涵蓋規則所要求的最佳實踐
  • 文件維護:確保參考資料保持準確與最新
  • Code Review:特別是 Google 的 Readability 流程——新進工程師透過 code review 被指導,培養風格指南要求的習慣和模式(詳見 Chapter 3)

自動化執行#

Google 強烈偏好以工具自動化執行規則,相較於依賴工程師手動驗證,自動化具有多項優勢:

  • 不會遺忘:新人加入、規則變更、專案擴展時,工具始終如一地執行
  • 消除解讀差異:所有輸入依據單一、不變的規則定義進行驗證,避免人為偏見
  • 可擴展:組織規模加倍時,執行成本幾乎不變

並非所有規則都能自動化。需要人為判斷的規則(如「避免複雜的模板元程式設計」、「auto 僅用於型別名稱嘈雜、顯而易見或不重要的情況」)以及社會性規則(如變更大小的建議),仍需依賴人為執行。

Error Checkers(錯誤檢查器)#

2018 年一項非正式調查估計,C++ 風格指南中約 90% 的規則可以自動驗證

Google 使用的工具包括:

  • clang-tidy:C++ 靜態分析
  • Error Prone:Java 靜態分析

這些工具在 code review 期間直接顯示違規警告和建議修復,大幅降低遵循規則的成本。當 Google 開始使用工具標記已棄用函式並就地顯示警告與修復建議後,新使用已棄用 API 的問題幾乎在一夜之間消失

Code Formatters(程式碼格式化器)#

Google 使用自動化格式化工具確保一致的格式:

語言格式化工具
C++clang-format
Pythonyapf(內部封裝)
Gogofmt
Dartdartfmt
BUILD 檔案buildifier

格式化透過 presubmit checks 強制執行:程式碼提交前,服務會檢查執行格式化器後是否產生差異,有差異則拒絕提交。

在管理史上最大程式碼庫的過程中,Google 觀察到自動化格式化的結果平均而言顯著優於人工格式化。除了少數領域(如矩陣格式化),自動化工具幾乎不會出錯。

案例:gofmt——Go 語言從第一天就內建標準格式化工具 gofmt,因為團隊知道開源發布後幾乎不可能再回頭制定標準格式。gofmt 沒有配置選項,行為極少改變。最初使用者抱怨強制標準,後來卻經常將 gofmt 列為喜歡 Go 的原因之一。標準化格式也為 gofix 等自動更新工具奠定了基礎——機器編輯的程式碼與人工編輯的程式碼無法區分,讓工程師能專注於審查真正重要的變更。

Google 在 2012 年也使用 buildifier 對 200,000 個 BUILD 檔案進行標準化格式化,一名工程師花了六週完成。

結論#

對於任何組織——尤其是像 Google 這樣規模的工程團隊——規則幫助管理複雜度並建構可維護的程式碼庫。一套共享的規則為工程流程提供框架,使其能夠擴展和持續成長,長期維持程式碼庫和組織的永續性。

TL;DRs#

  • 規則與指引應以支持時間韌性規模擴展為目標
  • 掌握數據,以便適時調整規則
  • 不是所有事情都該成為規則
  • 一致性是關鍵
  • 盡可能自動化執行