AI 代理总写过时的 Go:JetBrains 开源 go-modern-guidelines 拆解与实测

JetBrains 在 2025 年 11 月上线了一个面向 AI 编码代理的开源项目 go-modern-guidelines,Apache 2.0 协议,给 Claude Code、Codex、Cursor、Junie 等主流代理提供一套"现代 Go"写作规范。它的出发点是一个 Go 开发者都有体感的现象:AI 代理写出来的 Go 代码,往往停留在几年前的地方。本文拆解这个项目的工作机制,并用它的 CLI 实测各 Go 版本的规则数量,看"代理写出过时 Go"这个问题到底有多严重,以及 JetBrains 给出的解法是什么。

JetBrains/go-modern-guidelines 仓库

AI 为什么总写"过时"的 Go

JetBrains 在 README 里把原因归结为两条:训练数据滞后和频率偏置。

训练数据滞后容易理解。模型的知识有截止日期,Go 1.26 的 errors.AsType[T] 如果没出现在训练语料里,模型就不可能写出它。这一点决定了任何模型对新语言特性都有一段天然的无知期。

频率偏置更隐蔽,也更顽固。即使模型知道新特性,训练数据里 for i := 0; i < n; i++ 的出现次数也远多于 for i := range n,于是生成代码时旧写法被采样的概率更高。旧惯用法在存量代码里积累了几十年,这种统计惯性不会因为新版本发布而消失。结果是:代理写出的 Go 语法正确、能编译、能跑,但充斥着手写 min/max、手动循环查找、interface{} 这类早已被标准库和语言特性取代的模式。

JetBrains 的判断是这个问题应该在新代码产生时就解决,省掉将来的迁移。Go 官方有一个 modernize 分析器负责把存量代码自动升级到新惯用法,go-modern-guidelines 做的是同一件事的镜像:让代理写新代码时直接用现代写法,省掉将来的迁移。

一套版本感知的规则库,加一个本地 CLI

项目由两部分组成。

第一部分是规则库本身,覆盖 Go 1.0 到 1.27 的惯用法演进,重点是 Go 1.21 之后的批量更新:泛型配套的 slices/maps 包、内建 min/max、Go 1.22 的循环变量语义修正与整数 range、Go 1.23 的迭代器、Go 1.24 的 omitzero 与 b.Loop、Go 1.25 的 WaitGroup.Go 与 t.Context()、Go 1.26 的 new(value) 与 errors.AsType。FEATURES.md 里列出了 37 项诊断,按影响分级,其中 4 项 Critical(slices.Contains 替代手写查找循环、for i := range n 替代三段式循环、any 替代 interface{}、errors.Is 替代等号比较),15 项已经实现为 go fix 的 modernize 分析器,意味着存量代码可以自动改写,新代码则由代理直接写对。

第二部分是一个命令行工具。代理在编辑 Go 文件前先调用 list 子命令,CLI 从 go.mod、go.work 或本地工具链解析出项目的 Go 版本,返回该版本可用的规则清单,按新旧排序;需要细节时再用 explain 查具体条目的 before/after 示例。skill 文件里有一条严格的约束:明确禁止代理把 list 输出接进 head、tail 或 grep 过滤,因为清单靠前的条目不一定是代理该用的全部规则,截断读取会让仍然适用的一些规范被漏掉。

版本感知是整个设计的关键。一个还在用 Go 1.20 的项目不该收到 Go 1.24 的 omitzero 建议(编译都过不了),而升级到 1.26 的项目应该立刻拿到全部 48 条规则。规则清单随 go.mod 变化。这个设计面向的现实是:同一个团队里不同项目往往锁定在不同 Go 版本,而 AI 代理并不知道"这个仓库允许用什么"。

各版本有多少规则可用:实测数据

光看架构不够,我们把仓库克隆下来,编译出 CLI 二进制,对 Go 1.20 到 1.27 各版本实测 list 输出的规则数:

Go 版本可用规则数该版本新增的关键特性
1.2013errors.Join、CutPrefix/CutSuffix
1.2237循环变量语义修正、for range int、min/max
1.2341迭代器 range over func
1.2445omitzero、b.Loop()、generic type aliases
1.2546WaitGroup.Go、t.Context()、testing/synctest
1.2648new(expr)、errors.AsType[T]
1.2754json/v2、标准库 uuid、strings.CutLast

两个数字值得单独说。

第一个是 13 到 37 的跳变。一个停留在 Go 1.20 的项目,代理可用的现代惯用法只有 13 条;升到 1.22 立刻变成 37 条,接近三倍。这中间夹着 Go 近几年最密集的一次惯用法升级:slices/maps 标准库泛型包、内建 min/max、整数 range、循环变量按迭代作用域。跳过这段升级的项目,代理写的每一行代码都在错过这些。

第二个是 1.27 的 54 条,比 1.26 多出 6 条,其中 json/v2 和标准库 uuid 包是相当大的供给变化:JSON 序列化换新实现、UUID 不再需要 google/uuid 这样的第三方依赖。指南对 json/v2 的态度是"新代码用 v2,存量代码不动",这个边界划得比较克制。

顺带一提,4 条 Critical 级规则全部来自 1.22 及更早版本。换句话说,对一个现代 Go 项目(1.22 以上)来说,最重要的修正早就可用了,问题只剩代理知不知道。

Go 1.26 的新东西长什么样

我们在本机 Go 1.26.5 上编译验证了 README 提到的两个新特性,代码如下:

go
// Go 1.26: new(value) 直接从值创建指针
cfg := struct {
    Timeout *int
    Debug   *bool
}{Timeout: new(30), Debug: new(true)}

// Go 1.26: errors.AsType 返回匹配值与布尔,不再需要预先声明目标变量
if pe, ok := errors.AsType[*PathErr](work()); ok {
    fmt.Println("matched:", pe.p)
}

new(30) 解决的是一个 Go 社区吐槽了很多年的问题:给结构体指针字段赋值,要么预先声明变量再取地址,要么每个类型写一个 func ptr[T any](v T) *T 辅助函数(几乎所有项目都有一份这个函数的变体)。Go 1.26 扩展了 new 的语义,new(表达式) 直接返回指向该值的指针,辅助函数可以整体删掉。

errors.AsType[T] 优化的是错误处理样板。旧的 errors.As 模式需要先声明一个目标类型变量,再传它的指针,用返回值判断是否匹配;新 API 把"匹配"和"取值"合成一次调用,错误处理路径少两行,而且类型参数写法让目标类型一目了然。

再往前一个版本的 Go 1.25 贡献了 wg.Go

go
// Go 1.25: WaitGroup.Go 合并 Add/Done 样板
var wg sync.WaitGroup
for _, item := range items {
    wg.Go(func() { process(item) })
}
wg.Wait()

这三段代码全部在 Go 1.26.5 下编译运行通过。它们共同的特质是:删的都是纯样板,不改变任何语义。这正是"现代 Go"的方向——语言和标准库把开发者从仪式性代码里解放出来。

代理侧怎么接:四家工具,一套规范

安装方式上,go-modern-guidelines 同时提供四种代理的插件:

工具安装命令
Claude Code/plugin marketplace add JetBrains/go-modern-guidelines/plugin install modern-go-guidelines@goland-claude-marketplace
Codexcodex plugin marketplace add JetBrains/go-modern-guidelinescodex plugin add modern-go-guidelines@goland-codex-marketplace
Cursorcursor-agent plugin marketplace add https://github.com/JetBrains/go-modern-guidelines
Junie/extensions marketplace add JetBrains/go-modern-guidelines
其他(OpenCode 等)npx skills add JetBrains/go-modern-guidelines

所有分发形态最终都指向同一个 skill 文件,内容是一套严格的调用协议:编辑 Go 代码前必须先跑 list,读完完整输出再动手;对拿不准的规则用 explain 查示例;返回的规则视为权威,即使仓库现有代码用的是旧写法。最后一条相当激进——skill 明确要求代理新写的代码遵循现代规范,哪怕周围的存量代码全是旧风格,除非遵循规范会导致编译失败或行为变化。

这个选择背后有一个务实判断:让代理"入乡随俗"写旧代码,等于把训练数据的问题永久固化;指定"新代码必须现代",存量代码则交给 go fix 的 modernize 分析器渐进迁移。两条线分工清楚。

对 Go 团队方向的印证

go-modern-guidelines 与 Go 官方工具链的演进是同一件事的两半。Go 团队自己做过一场关于 modernize 分析器的分享,方向就是用工具把存量代码批量迁到新惯用法;标准库近年新增的 slices、maps、cmp 包,以及 min/max 内建函数,也都在降低"用新写法"的门槛。

JetBrains 补上的是"新代码"这一半:modernize 负责过去,guidelines 负责当下。两者覆盖同一份规则表(37 项诊断中 15 项已实现为 modernize 分析器),但作用在代码生命周期的不同阶段。

对普通 Go 开发者,这个项目最直接的用法有三个:

  1. 给代理装上它。用 Claude Code、Codex 或 Cursor 的话,按上表装插件即可,要求本机有 Go 工具链(CLI 首次使用时 go install 到本地缓存)。目标版本 Go 1.25 以上;老版本依赖 GOTOOLCHAIN=auto(默认开启)自动拉取兼容工具链。
  2. 把存量代码过一遍 modernizego fix 的 modernize 分析器能自动完成 15 项高频修正,配合 Critical 级的四条规则,一个中型项目通常能删掉成百上千行样板。
  3. 升级 Go 版本。上表的规则数曲线说明,每升一个版本,代理可用的惯用法就多一批。停在 1.20/1.21 的项目,代理永远写不出 1.22 之后的任何东西。

结语

AI 代理写出过时代码的问题,本质是统计学习与时效性的矛盾:模型从历史数据学习,而编程语言在向前走。go-modern-guidelines 的价值在于把"提示词修复"做成了版本感知的工程系统——规则随 go.mod 演进,CLI 是版本解析的事实来源,分发走各家代理的插件市场。48 条规则、13 种类别、4 级影响分级的结构化程度,让它可以随 Go 版本持续扩展。对每天让代理写 Go 的团队,这是目前成本最低的"让 AI 写现代 Go"方案。

来源: