golang-code-style

golang-code-style

熱門

Golang 程式碼風格慣例 — 行長度與斷行、變數宣告、控制流程清晰度、何時該加註解。用於撰寫或審查 Go 程式碼、詢問風格或清晰度問題、或建立專案編碼標準。不適用於命名慣例(→ 請參閱 `samber/cc-skills-golang@golang-naming` 技能)、linter 配置(→ 請參閱 `samber/cc-skills-golang@golang-lint` 技能)或文件註解(→ 請參閱 `samber/cc-skills-golang@golang-documentation` 技能)。

2261星標
150分支
更新於 2026/6/6
SKILL.md
readonlyread-only
name
golang-code-style
description

Golang 程式碼風格慣例 — 行長度與斷行、變數宣告、控制流程清晰度、何時該加註解。用於撰寫或審查 Go 程式碼、詢問風格或清晰度問題、或建立專案編碼標準。不適用於命名慣例(→ 請參閱 `samber/cc-skills-golang@golang-naming` 技能)、linter 配置(→ 請參閱 `samber/cc-skills-golang@golang-lint` 技能)或文件註解(→ 請參閱 `samber/cc-skills-golang@golang-documentation` 技能)。

社群預設。 若公司技能明確取代 samber/cc-skills-golang@golang-code-style 技能,則以公司技能為準。

Go 程式碼風格

需要人為判斷的風格規則 — linter 處理格式化,此技能處理清晰度。命名請參閱 samber/cc-skills-golang@golang-naming 技能;設計模式請參閱 samber/cc-skills-golang@golang-design-patterns 技能;結構/介面設計請參閱 samber/cc-skills-golang@golang-structs-interfaces 技能。

「清晰勝於巧妙。」— Go 諺語

若忽略某條規則,請在程式碼中加入註解。

行長度與斷行

沒有嚴格的行數限制,但超過約 120 個字元的行必須斷行。應在語意邊界處斷行,而非任意欄位數。函式呼叫有 4 個以上引數時,必須每個引數一行 — 即使提示要求單行程式碼:

// 好 — 每個引數獨立一行,右括號分開
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
    handleUsers(
        w,
        r,
        serviceName,
        cfg,
        logger,
        authMiddleware,
    )
})

當函式簽名過長時,真正的解決方案通常是減少參數(使用選項結構),而非更好的換行。對於多行簽名,每個參數應獨立一行。

變數宣告

非零值應使用 :=,零值初始化應使用 var。此形式傳達意圖:var 表示「從零開始」。

var count int              // 零值,稍後設定
name := "default"          // 非零值,適合用 :=
var buf bytes.Buffer       // 零值即可直接使用

切片與映射初始化

切片和映射必須明確初始化,絕不能為 nil。Nil 映射在寫入時會 panic;nil 切片在 JSON 中序列化為 null(相對於空切片的 []),會讓 API 使用者感到意外。

users := []User{}                       // 總是初始化
m := map[string]int{}                   // 總是初始化
users := make([]User, 0, len(ids))      // 已知容量時預先分配
m := make(map[string]int, len(items))   // 已知大小時預先分配

不要投機性地預先分配 — 當常見情況只有 10 個項目時,make([]T, 0, 1000) 會浪費記憶體。

複合字面值

複合字面值必須使用欄位名稱 — 位置欄位在型別新增或重新排序欄位時會出錯:

srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}

控制流程

減少巢狀結構

錯誤和邊界情況必須優先處理(及早 return)。保持快樂路徑在最小縮排:

func process(data []byte) (*Result, error) {
    if len(data) == 0 {
        return nil, errors.New("empty data")
    }

    parsed, err := parse(data)
    if err != nil {
        return nil, fmt.Errorf("parsing: %w", err)
    }

    return transform(parsed), nil
}

消除不必要的 else

if 主體以 return/break/continue 結束時,必須移除 else。對於簡單的賦值,使用預設值再覆寫 — 先賦予預設值,再用獨立條件或 switch 覆寫:

// 好 — 預設值再以 switch 覆寫(互斥覆寫時最乾淨)
level := slog.LevelInfo
switch {
case debug:
    level = slog.LevelDebug
case verbose:
    level = slog.LevelWarn
}

// 差 — else-if 鏈隱藏了有預設值的事實
if debug {
    level = slog.LevelDebug
} else if verbose {
    level = slog.LevelWarn
} else {
    level = slog.LevelInfo
}

複雜條件與初始化範圍

if 條件有 3 個以上運算元時,必須提取為具名布林值 — 一堆 || 難以閱讀且隱藏了商業邏輯。將昂貴的檢查保留在行內以獲得短路效益。詳細資訊

// 好 — 具名布林值讓意圖清晰
isAdmin := user.Role == RoleAdmin
isOwner := resource.OwnerID == user.ID
isPublicVerified := resource.IsPublic && user.IsVerified
if isAdmin || isOwner || isPublicVerified || permissions.Contains(PermOverride) {
    allow()
}

將變數範圍限定在 if 區塊內,僅在檢查時需要:

if err := validate(input); err != nil {
    return err
}

使用 Switch 取代 If-Else 鏈

當多次比較同一個變數時,優先使用 switch

switch status {
case StatusActive:
    activate()
case StatusInactive:
    deactivate()
default:
    panic(fmt.Sprintf("unexpected status: %d", status))
}

函式設計

  • 函式應簡短且專注 — 一個函式只做一件事。
  • 函式應有**≤4 個參數**。超過時,使用選項結構(請參閱 samber/cc-skills-golang@golang-design-patterns 技能)。
  • 參數順序context.Context 優先,接著是輸入,最後是輸出目標。
  • 裸 return 在非常短的函式(1-3 行)中有幫助,因為回傳值顯而易見,但當讀者需要滾動才能找到回傳值時會造成混淆 — 在較長的函式中明確命名回傳值。
func FetchUser(ctx context.Context, id string) (*User, error)
func SendEmail(ctx context.Context, msg EmailMessage) error  // 分組到結構中

優先使用 range 進行迭代

應使用 range 而非基於索引的迴圈。使用 range n(Go 1.22+)進行簡單計數。

for _, user := range users {
    process(user)
}

值與指標引數

小型型別(stringintbooltime.Time)以值傳遞。在需要修改、大型結構(約 128+ 位元組)或 nil 有意義時使用指標。詳細資訊

檔案內程式碼組織

  • 將相關宣告分組:型別、建構子、方法放在一起
  • 順序:套件文件、匯入、常數、型別、建構子、方法、輔助函式
  • 每個檔案一個主要型別,當該型別有重要方法時
  • 空白匯入_ "pkg")註冊副作用(init 函式)。將其限制在 main 和測試套件中,讓副作用在應用程式根層級可見,而非隱藏在函式庫程式碼中
  • 點匯入會污染命名空間,且無法判斷名稱來自何處 — 絕不在函式庫程式碼中使用
  • 積極不匯出 — 你隨時可以匯出;取消匯出是破壞性變更

字串處理

使用 strconv 進行簡單轉換(較快),使用 fmt.Sprintf 進行複雜格式化。在錯誤訊息中使用 %q 讓字串邊界可見。在迴圈中使用 strings.Builder,簡單串接使用 +

型別轉換

優先使用明確、狹義的轉換。當具體型別可行時,使用泛型而非 any

func Contains[T comparable](slice []T, target T) bool  // 不是 []any

哲學

  • 「一點複製勝過一點依賴」
  • 使用 slicesmaps 標準套件;對於 filter/group-by/chunk,使用 github.com/samber/lo
  • 「反射永遠不清晰」 — 除非必要,否則避免使用 reflect
  • 不要過早抽象化 — 在模式穩定後再提取
  • 最小化公開表面 — 每個匯出的名稱都是一項承諾

並行化程式碼風格審查

在大型程式碼庫中審查程式碼風格時,可使用最多 5 個並行子代理(透過 Agent 工具),每個子代理針對一個獨立的風格關注點(例如控制流程、函式設計、變數宣告、字串處理、程式碼組織)。

使用 Linter 強制執行

許多規則會自動強制執行:gofmtgofumptgoimportsgocriticrevivewsl_v5。→ 請參閱 samber/cc-skills-golang@golang-lint 技能。

交叉參考

  • → 請參閱 samber/cc-skills-golang@golang-naming 技能了解識別碼命名慣例
  • → 請參閱 samber/cc-skills-golang@golang-structs-interfaces 技能了解指標與值接收器、介面設計
  • → 請參閱 samber/cc-skills-golang@golang-design-patterns 技能了解函式選項、建構器、建構子
  • → 請參閱 samber/cc-skills-golang@golang-lint 技能了解自動格式化強制執行
  • → 請參閱 samber/cc-skills-golang@golang-continuous-integration 技能了解在 CI 中使用這些指南進行自動化 AI 驅動的程式碼審查