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` 技能)。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-code-style
描述

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))   // 已知大小时预分配

不要投机性地预分配——make([]T, 0, 1000) 在常见情况只有 10 个元素时会浪费内存。

复合字面量

复合字面量必须使用字段名——位置字段在类型添加或重排字段时会出错:

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

控制流

减少嵌套

错误和边界情况必须首先处理(提前返回)。保持快乐路径在最小缩进:

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 在前,然后是输入,最后是输出目标。
  • 裸返回在非常短的函数(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 强制执行

许多规则由 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 驱动的自动代码审查