Community编程与开发github.com

davidvella/go-style

编写、审查或重构易读且快速的 Go 代码。保持函数小而低复杂度,将性能设计融入 API 与数据布局,并用基准测试、分配计数和逃逸分析验证改动。

go-style 是什么?

该 AI Agent 技能专注于按 Go 语言社区推崇的“可读且高性能”风格编写、审查与重构代码。其核心理念源自 Dave Cheney 的“API 设计作为性能原语”,主张超过一定程度后性能无法事后添加,应前置设计;同时遵循 John Ousterhout“将错误定义出存在”等原则。技能支持在所有主流 Go 开发环境(CLI、编辑器插件、CI/CD 中调用的 AI 代理)中使用。工作方式为:先确保正确性,再优化 API 形态与大 O 复杂度,然后削减热路径上的内存分配,最后才在经剖面分析证实的瓶颈处做微优化。它将冷热路径分离,提倡返回视图而非副本、让调用者持有可复用缓冲区、用迭代器模式将错误排除在热循环之外,并通过分层 API 提供无分配的基础构件与上层便利接口。同时要求每项性能改动必须用 benchstat 比较、go build -gcflags=-m 检查逃逸、testing.AllocsPerRun 验证零分配承诺。最终产出的代码函数小巧扁平(圈复杂度 ≤10,认知复杂度 ≤10),数据布局优先,并严格遵循 gofmt、go vet 等 Go 惯例。

兼容平台~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 形状与大 O。 谁拥有内存、返回什么、每次调用做多少工作。这些比任何后续调优更能决定性能。
  3. 热路径上的分配。 每次分配成本双倍:一次用于分配,一次用于垃圾回收器。回收器可能很快,“但不分配永远更快”。
  4. 每单位输入的工作。 每个字节只触摸一次,按令牌或记录调用,而不是按字节调用。
  5. 微优化(手动内联、位技巧、避免边界检查),仅在剖面分析显示为热路径的路径上,且必须有基准测试证明收益。

如果更改未在更高层级得到证明,就不要进行。

分离热路径和冷路径

确定哪些代码每次事件或请求都运行(热路径),哪些在设置、构建、配置或更新时运行(冷路径)。

  • 冷路径: 编写最直接的代码。克隆而非共享,使用 map 和 fmt,自由分配。不要在此引入热路径惯用法;它们牺牲可读性但毫无收获。
  • 热路径: 可以使用可复用缓冲区、代计数器、扁平数组等技术。为每种技术赋予一个名称(一个小类型或辅助函数)并附上注释解释其存在原因,这样读者无需逆向工程。

为性能设计 API

API 决定了调用者可能节省多少。“你在对垃圾回收器做基准测试”当 API 强制每个结果在堆上分配时。设计 API 使得快路径是自然路径;2-10 倍的提升通常可用,“如果你愿意接受不同的 API”。

  • 不要返回必须逃逸的值。 通过 interface{}/any、新字符串、map 或指向每个项的新结构体的指针返回原始类型,每次调用都会强制分配(encoding/json.Decoder.Token 是经典的“垃圾工厂”)。返回具体类型,或返回视图如 []byte,让调用者只转换它需要的部分。
  • 返回视图,而非副本, 当调用者只需要读取时。在文档注释中说明视图的有效期(“直到下次调用有效”,如 bufio.Scanner.Bytes)。直接返回输入的原始视图(令牌的字节,其首字节给出类别)比调用者可能忽略的解码值更廉价。
  • 让调用者拥有可复用内存。 接受 scratch 或缓冲区参数,或将其保存在调用者持有的结构体中,而不是每次调用都分配。让调用者提供初始缓冲区,使其可存活在栈上或来自对象池: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 视为其他任何终止条件。
  • 计算每单位输入的工作。 每个字节处理一次,按令牌或记录调用函数而非按字节,并消除中间副本。从逐字节调用变为逐令牌调用通常是消除分配之后的第二大收益。
  • 分层设计 API。 提供低层、无分配的基础构件(原始令牌、视图、拥有 scratch 的迭代器),并在其上构建便利的 API。不要让便利且会分配内存的 API 成为唯一的 API。
  • 在文档注释中写入所有权和生命周期: 哪些是借用的、哪些是保留的、哪些是并发安全的、零值有何含义。
  • 在自然的情况下让零值有用。
  • 优先选择不可变的共享值并发布新快照,而非在共享可变状态上加锁,当读者占主导时。

不仅要度量实现,还要度量 API:比较 allocs/op 和吞吐量与您要替换的 API。references/examples.md 并排展示了这些形状。

调优热循环,最后一步

仅在 API 和大 O 都正确,且剖面分析显示循环为热路径之后:

  • 对一个状态的 switch 编译为一连串比较。通过 func 字段(d.state = (*Decoder).stateValue)为每个状态选择的小方法,保持每个状态的可读性,并可能消除该调度。
  • 将检查移至使用该值的函数中,以便编译器能证明边界检查不必要,并保持入口在内联预算 (80) 内;一次调用大约消耗 52)。使用 go build -gcflags=-m=2 检查。
  • 合并两个函数以避免一次调用,在每条令牌路径上可能赢得 5-15% 的收益,代价是更深层的嵌套。仅在基准测试证实后这样做,并在注释中说明原因。

正如 Cheney 所说:不要“在没有解决 API 中的大 O 效应之前”就使用这些技巧。“它不会改变现状。”

保持函数小巧且扁平

目标是每个函数做一件事,至多有一到两个分支。作为指导,将圈复杂度保持在 10 以下,认知复杂度保持在 10 以下(gocyclo,gocognit)。一个扁平的 type switch 将每个 case 交给一个调用是允许的,但嵌套则不行。

  • 为多条件命名。 将 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 索引的四个切片就是一个结构体。它读起来更好,并且通常能改善局部性。
  • 为重复出现的机制赋予自己的小型类型。 如果相同的计数器回绕、位图或缓冲区逻辑出现两次,则它就是包含两三个方法的类型。
  • 优先选择显式字段而非位打包, 除非经过测量后打包确实重要。[]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 规则;仅当有命名的规则和原因时,才禁止单一行。
  • 短的接收者名称,在类型的所有方法间保持一致;没有理由的话,不要在一个类型上混用指针和值接收者。
  • 文档注释以标识符名称开头,并说明约定。
  • 注释解释为什么,而不是做了什么。与周围注释密度匹配。
  • 优先使用标准库(slices,maps,strings,math/bits),然后才是手写辅助函数。
  • 绝不要手动编辑生成的代码。

审查或重构时

  1. 在接触任何内容之前,先确定热路径及其基准测试。
  2. 测量复杂度以定位最差的函数;优先修复这些。
  3. 以小步骤进行重构,每一步后运行专注的测试。
  4. 保持行为完全一致:相同的结果、相同的预算和后备、相同的分配保证。仅作为一个单独的、明确声明的决定来改变行为。
  5. 报告发生了什么变化、前后复杂度与基准测试数据,以及任何未经验证的内容。

相关技能