Community程式設計與開發github.com

davidvella/go-style

撰寫、審查或重構易於閱讀且高效能的 Go 程式碼。保持函式短小、低循環複雜度與認知複雜度,將效能體現在 API 設計與資料佈局而非取巧的程式碼,並透過基準測試、分配次數與逸出分析來驗證變更。

go-style 是什麼?

這個技能專注於撰寫兼具可讀性與高效能的 Go 程式碼。其核心理念是:透過 API 設計與資料佈局取得效能,而非依賴散落各處的取巧技巧。技能明確定義了優先順序:正確性優先於速度,其次才是 API 形狀與 big-O、熱路徑分配、每單位輸入的工作量,最後才是微優化。它提供了具體的準則,例如區分熱路徑與冷路徑、設計以效能為考量(避免強制在堆上分配、回傳視圖而非副本、讓呼叫者擁有可重複使用的記憶體)、用層次化 API 將低階分配自由建構塊與高階便利 API 分開。此技能支援典型的 Go 開發工作流程,並依賴實務工具如 `go vet`、`gocyclo`、`gocognit` 與 `benchstat` 來強制執行這些規範。其理念深受 Dave Cheney「把 API 設計當成效能基本單元」及 John Ousterhout「將錯誤定義在存在之外」的影響。它不僅適用於編寫原始碼,也適用於 code review 與重構,強調以基準測試資料(透過比較分配次數、吞吐量與 gcflags 逸出輸出)來驗證任何宣稱「更快」或「更少分配」的變更。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/davidvella/skills/tree/HEAD/skills/go-style

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Go 風格:可讀且快速

目標是寫出維護者能從頭到尾讀懂的程式碼,其效能來自於 API 與資料的形狀,而非散落在程式碼中的取巧技巧。一旦設計正確,可讀性與速度很少會衝突;若兩者真的衝突,只有在經過量測的熱路徑上才放棄可讀性,並在註解中說明。

此風格遵循 Dave Cheney 的「把 API 設計當成效能基本單元」:超過某個臨界點後,效能無法事後補上,因此應預先設計,其餘部分保持簡潔。

來源:Cheney 的演講 (https://dave.cheney.net/paste/gophercon-uk-2026.html),程式碼在 github.com/pkg/json 與 github.com/davecheney/bytereader;John Ousterhout,「將錯誤定義在存在之外」。

優先順序

  1. 正確性。 永遠不要為了速度或簡潔而犧牲它。
  2. API 形狀與 big-O。 誰擁有記憶體、回傳什麼、每次呼叫做多少工作。這些比任何後續調校更能決定效能。
  3. 熱路徑上的分配。 每次分配耗費兩次成本:一次分配,一次供垃圾收集器處理。收集器或許很快,「但永遠不會比不分配快。」
  4. 每單位輸入的工作量。 每個位元組只處理一次,並以每個 token 或記錄為單位進行函式呼叫,而非每個位元組。
  5. 微優化(手動內聯、位元技巧、避免邊界檢查),僅在剖析顯示為熱路徑上進行,且必須有基準測試證明其效益。

如果一項變更在上層級別中沒有被證明是合理的,就不要做。

區隔熱路徑與冷路徑

判斷哪些程式碼在每次事件或請求時執行(熱路徑),哪些在設定、建置、配置或更新時執行(冷路徑)。

  • 冷路徑: 寫最簡單的程式碼。使用複製而非共用、使用 map 與 fmt、自由地分配。不要在此引入熱路徑的慣用語法,它們會犧牲可讀性卻毫無助益。
  • 熱路徑: 可以使用可重複使用的緩衝區、世代計數器、平面陣列及類似技術。為每種技術命名(一個小型別或輔助函式)並加上註解解釋其存在原因,讓讀者不必逆向工程。

以效能為考量設計 API

API 決定了呼叫端最多能節省多少。「你是在對垃圾收集器做基準測試」當 API 強迫每個結果都放到堆上時。將 API 塑造得讓快速路徑是自然的路徑;「如果你準備好接受不同的 API」,通常可獲得 2-10 倍的效能提升。

  • 不要回傳必須逸出的值。 透過 interface{}/any、全新的字串、map 或指向每個項目新結構的指標來回傳原始型別,會迫使每次呼叫都分配一次記憶體(encoding/json.Decoder.Token 是經典的「垃圾工廠」)。回傳具體型別,或類似 []byte 的視圖,讓呼叫端只轉換它需要的部分。
  • 回傳視圖而非副本, 當呼叫端只需要讀取時。記錄視圖的有效時間(「在下一次呼叫前有效」,類似 bufio.Scanner.Bytes)。原始輸入的視圖(token 的位元組,其第一個位元組指示其種類)比一個呼叫端可能忽略的解碼值更節省。
  • 讓呼叫者擁有可重複使用的記憶體。 接受一個暫存區或緩衝區參數,或將其保存在由呼叫者維護的結構中,而不是在每次呼叫時分配。讓呼叫者提供初始緩衝區,使其可存在於堆疊或來自物件池:var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:])。
  • 透過少數方法隱藏緩衝區管理。 io.Reader 會複製到呼叫者管理的緩衝區,並將重新填滿的責任留給每個呼叫者。一個小型視窗 API 將資料保留在原位:使用 Window() []byte 查看,Release(n) 消耗,Extend() int 讀取更多,並在文件註解中說明「視窗在 Release 或 Extend 後失效」。
  • 將錯誤定義在熱迴圈之外。 使用回傳 bool 或空結果並在之後報告錯誤的迭代方式:for s.Next() { ... } 然後 s.Err()。在慢路徑上使用哨兵結果(Extend 回傳 0,然後呼叫者詢問錯誤),使內部迴圈沒有錯誤分支。將 io.EOF 視為任何其他的終止條件。
  • 計算每單位輸入的工作量。 每個位元組只處理一次,在每個 token 或記錄層級進行函式呼叫,而非每個位元組,並移除中間複本。從逐位元組呼叫改為逐 token 呼叫,往往是僅次於移除分配的第二大效能提升。
  • 將 API 分層。 提供一個低階、無分配的建構塊(原始 token、視圖、擁有暫存區的迭代器),並在其上建構便利的 API。不要只提供便利但會分配的 API。
  • 在文件註解中寫下所有權與生命週期: 哪些是借用的、哪些會保留、哪些可安全用於並發、零值的意義。
  • 讓零值有用, 在自然的情況下。
  • 偏好不可變的共用值 並發布新的快照,勝過在共用可變狀態上使用鎖,當讀取操作佔多數時。

為 API 做基準測試,而不僅僅是實作:比較 allocs/op 和吞吐量,與您正在取代的 API 相比。references/examples.md 並排展示了這些形狀。

調校熱迴圈,最後才做

只有在 API 和 big-O 都正確,且剖析顯示該迴圈是熱的之後:

  • 在狀態上的 switch 會編譯成一串比較。每個狀態使用一個小型方法,透過 func 欄位選擇(d.state = (*Decoder).stateValue),使每個狀態保持可讀,並可消除那個分派。
  • 將檢查移到使用該值的函式中,讓編譯器能證明邊界檢查是不必要的,並將入口點維持在內聯預算內(80;一次呼叫成本約為 52)。使用 go build -gcflags=-m=2 檢查。
  • 合併兩個函式以避免一次呼叫,可以在每個 token 路徑上獲得 5-15% 的效益,但代價是更深的巢狀結構。只在有基準測試的情況下才這麼做,並在註解中說明原因。

正如 Cheney 所言:不要「在未解決 API 中的 big O 效應之前」就使用這些技巧。「它不會讓指針移動。」

保持函式短小且扁平

目標是每個函式只做一件事,最多一到兩個分支。作為指引,將循環複雜度維持在 10 或以下,認知複雜度維持在 10 或以下(使用 gocyclo、gocognit)。一個將每個 case 分配給一個呼叫的扁平型別 switch 是可以接受的,但巢狀就不行。

  • 為多項條件命名。 將 if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM 替換為 if !fitsProduct(a, b)。名稱記錄了意圖,而函式可以承載解釋。
  • 分派,然後委派。 針對節點或訊息種類的 switch 應該為每個 case 呼叫一個方法,而不是包含每個 case 的邏輯。
  • 及早回傳。 先處理警衛條件,主路徑不縮排。
  • 不要重複 case。 如果兩個分支僅在運算元順序上不同,寫一個輔助函式並呼叫兩次。
  • 拆分混合的關注點。「找到它」、「建構新值」和「整理」是三個不同的函式,不是一個。
  • 避免布林旗標參數。 如果布林旗標是最清楚的選項,給它一個有意義的名稱,並在呼叫點處加以說明。
  • 使用查找表(map[string]Kind、[256]bool)取代字串或位元組比較鏈。

為讀者與 CPU 塑造資料

  • 將平行切片分組為結構體切片。 四個由相同 ID 索引的切片是一個結構體。這讀起來更好,且通常能改善位置 locality。
  • 為重複的機制賦予其小型型別。 如果相同的計數器環繞、點陣圖或緩衝區邏輯出現兩次,它就是一個有兩到三個方法的型別。
  • 偏好顯式欄位勝過位元打包, 除非量測顯示打包會影響效能。[]segment{name, each} 勝過一個帶有位移運算的 uint64 旗標。
  • 保持少的表示方式。 更少的狀態或種類意味著更少重複的 switch。在沒有顯著效能成本的情況下,將特殊情況合併到一般情況中。

證明效能宣稱

永遠不要沒有量測就宣稱一項變更更快、更慢或無分配。在修改效能敏感的程式碼之前:

  1. 儲存相關基準測試的 benchstat 基準線。
  2. 進行變更,然後進行比較。用數字報告顯著的差異。
  3. 將 go build -gcflags=-m 的輸出與基準線進行差異比較,查看新的堆逸出。
  4. 對於承諾零分配的路徑,保留或新增 testing.AllocsPerRun 測試。
  5. 在猜測回歸原因之前先進行剖析。

如果簡化會花費可量測的熱路徑時間,且你無法乾淨地回復,就停下來報告這個取捨,而不是悄悄地增加複雜性或默默地接受回歸。

閱讀 references/verify.md 了解確切的命令,包括如何量測複雜度以及通常會導致回歸的陷阱。閱讀 references/examples.md 了解展示此風格的重構前後範例。

遵循 Go 慣例

  • gofmt、go vet 及專案的 linter 必須通過。修正程式碼,不要削弱或排除 linter 規則;僅在特定行使用具名規則和原因來抑制。
  • 使用簡短的接收者名稱,在一個型別的所有方法間保持一致;若無理由,不要在一個型別上混合使用指標與值接收者。
  • 文件註解以識別碼名稱開頭,並陳述合約。
  • 註解要寫原因(why),而非內容(what)。與周圍的註解密度匹配。
  • 優先使用標準函式庫(slices、maps、strings、math/bits),然後才是手寫輔助函式。
  • 絕對不要手動編輯產生的程式碼。

審查或重構時

  1. 在觸碰任何東西之前,先識別熱路徑及其基準測試。
  2. 量測複雜度以找出最差的函式;優先修正它們。
  3. 以小步驟重構,每一步之後都執行有針對性的測試。
  4. 保持行為相同:相同的結果、相同的預算與後備方案、相同的分配保證。只有在獨立、明確聲明的決策下才改變行為。
  5. 報告什麼改變了、前後的複雜度與基準測試數字,以及任何未經驗證的事項。

相關技能