大部分的程式語言其實對於排版這件事情是很自由的,拿cpp來舉例
例子A:
#include <iostream>
using namespace std;
int main(){int x=10;int y=5;return x+y;}
例子B:
#include <iostream>
using namespace std;
int main(){
int x = 10;
int y = 5;
return x + y;
}
A 與 B 的格式不同,但程式邏輯完全相同:編譯出來的結果一樣,執行結果也一樣。差別只在人讀起來的樣子。
而這兩個例子只示範了換行與縮排一種面向。實務上的風格約定還包含縮排用空格還是 tab、每行長度上限、大括號要不要換行、include 的排列順序、以及變數與函式的命名慣例... 等等等。
拿 Google 當例子,Google C++ Style Guide,更能體會在程式風格上,需要約束的東西是非常多的。
Formatter 顧名思義為格式化工具。他能主動一鍵執行,也能在auto save時候就幫你強制執行。
在以前還沒有AI時,算是多人開發一定要帶入的規則,主要是為了解決多人開發時風格上的mismatch。解決mismatch有啥好處呢?昨天有介紹到 git diff 能顯示出目前程式與另一個branch之間的差異,這個差異是不分風格or程式邏輯的。
又 相比於風格,在多人開發 or code review 時更在意的是程式邏輯之間的差異,於是 formatter 就是為了此而生,它能夠在不影響程式邏輯的條件下,自動將風格之間的差異抹除。
那套用到現今時代,即使單人開發 formatter 也重要的原因是:coding agent 其實是具有不同的風格,即使是同一個 model,由於具有隨機性的緣故 風格也無法達到100%一致。
在日常使用 coding agent 時,agent 也常使用 git diff 幫我們檢查程式邏輯上的差異。透過 formatter 能將風格上的差異從 Context 裡移除,讓 coding agent 專注於邏輯的不同上,藉此提高其回覆與生成的品質。
一個專案要讓 formatter 實際發揮作用,需要三件事:工具本身、一套規則、以及執行它的時機。
下表列出幾個常見語言的主流選擇,依設定彈性由低到高排列:
| 語言 | 主要 formatter | 取得方式 | 設定彈性 |
|---|---|---|---|
| Go | gofmt |
官方工具鏈附帶 | 沒有設定檔 |
| Terraform | terraform fmt |
CLI 內建 | 沒有設定檔 |
| Dart | dart format |
SDK 附帶 | 極少 |
| Rust | rustfmt |
官方工具鏈附帶 | 少量選項 |
| Python | black |
另外安裝 | 少量選項 |
| JavaScript / TypeScript | Prettier | 另外安裝 | 少量選項 |
| Java | google-java-format |
另外安裝 | 少量選項 |
| C / C++ | clang-format |
另外安裝 | 上百個選項 |
同一個語言常有多個工具可選,上表取的是比較常見的主流。挑選時優先採用語言官方或主流的那一個,因為它決定了你之後能不能直接沿用別人的設定與慣例。
規則放在專案裡的設定檔,例如 clang-format 讀 .clang-format、Prettier 讀 .prettierrc。把設定檔一起提交進版本控制,是這件事的關鍵:規則因此變成專案的性質,而不是每個開發者各自的編輯器偏好。任何人 clone 下來執行 formatter,都會得到相同結果。
選項多的工具,實務上多半先套用一個 preset 再微調,例如在 .clang-format 寫上 BasedOnStyle: Google,就直接對應到前面提到的 Google C++ Style Guide。clang-format 設定選項 選項少的工具連這一步都省了——gofmt 沒有設定檔,Go 不提供調整風格的選項。gofmt 文件
這裡有個值得注意的傾向:越晚出現的語言,越傾向不讓你選。 Go、Rust、Dart 都把 formatter 收進官方工具鏈並大幅限制設定;C++、JavaScript 這些較早的生態則累積出多套互相競爭、高度可設定的工具。這不是工具成熟度的差別,而是取捨的位置不同:可設定的工具讓你符合既有約定,不可設定的工具讓你不必再約定。Go 的立場寫在 Go Proverbs 裡——「gofmt 的風格不是任何人的最愛,但 gofmt 是每個人的最愛」。
多數 formatter 另外提供只檢查不修改的模式(例如 clang-format --dry-run --Werror、black --check),遇到不符合規則的檔案就以失敗結束。這個模式是自動化檢查的基礎,明天要談的 pre-commit hook 用的就是它。
與 coding agent 合作時,formatter 需要你做的設定只有一件,換來的是兩個結果。
設定:把格式化指令寫進專案的 agent 設定檔。多數 coding agent 會讀取專案根目錄的設定檔(例如 CLAUDE.md、AGENTS.md),在裡面寫明「修改程式後執行格式化指令」,agent 每次改完就會自己執行。這比在 prompt 裡描述風格可靠:描述要靠 agent 每次都記得並正確套用,指令的結果則由工具決定。
結果一:diff 只剩下邏輯改動。agent 修改一個 function 時,常會一併動到周圍程式的縮排、換行或引號。這些調整經過 formatter 會被壓成既定格式,不會進到 diff 裡。要求 agent 修正自己的產出時尤其有用——第二輪的 diff 才看得出它這次改了什麼。
結果二:減少沒有內容的衝突。兩個 agent 在不同 branch 上各自重排了同一段程式,Git 會判定成雙方都修改了同一行,整合時產生衝突。這種衝突沒有邏輯可以判斷,卻要花時間處理。
這些做法的前提是專案已經有 formatter。既有專案第一次執行會改動幾乎每一個檔案,產生一個沒有人審閱得完的 commit,所以導入時要單獨提交、不夾帶任何功能修改。這個麻煩只發生一次,專案越大越痛——要導入就越早越便宜。
不過 formatter 保證的是風格一致,不是程式正確,也不是容易閱讀。命名不精確、function 過長、參數過多的程式,formatter 一樣讓它原封不動通過——它只處理能用文字規則判斷的那一層。
本篇談的 formatter 指能自動改寫程式碼、且保證不改變程式行為的工具。實務上有些工具同時做格式化與其他檢查,例如 ruff、eslint、rubocop,它們的部分規則已經超出排版,修正後也不一定保持行為不變。明天再來繼續介紹~