gopls v0.24.0 的交互式重构:以前加 struct tag 只能默认 json + camelCase
gopls 加入了交互式重构(interactive refactoring),背后是 LSP 的一项新提案:让语言服务器在计算文本编辑之前,先弹出一个可多步交互的对话框。这个能力由 gopls 团队与 Dart 团队合作设计,目标不止于 Go,而是作为所有语言、所有编辑器生态的开放标准。目前 gopls v0.24.0 与官方 VS Code Go 扩展 v0.56.0 已可用。
原文:Interactive refactoring in gopls(Hongxiang Jiang,16 October 2026)
以前为什么只能给默认值
LSP 把语言相关的能力与具体编辑器解耦:编辑器(VS Code、Neovim、Emacs)和语言服务器(gopls)通过持续的 JSON-RPC 交换提供补全、hover 文档、诊断、code action 等功能。
但在 LSP 里,几乎每一种代码变换都相当于一个无参函数:你触发操作,服务器立刻算出编辑。唯一的例外是重命名符号——它有一个参数(新名字),所以协议里专门给它开了 PrepareRename RPC。
这意味着语言服务器只能从上下文推断用户意图,也就是光标位置和所在语法树。上下文不够时,服务器只剩两条路:套一个一刀切的默认值,或者干脆不提供这个重构。限制不在 gopls 算不算得出编辑,而在 LSP 没有让服务器向用户提问的机制。
很多变换用无参模型跑得很好,但另一些必须要用户输入:加哪些 struct tag、用什么命名约定、实现哪个接口、把声明移到哪个文件。没有提问通道时,gopls 只能猜——「Add struct tags」永远产出 camelCase 的 json tag——或者直接不给。
有些语言扩展把自定义的、语言专属的对话框逻辑硬编码进各个编辑器,代价是 Neovim、Emacs 这类严格按 LSP 走、几乎不写客户端代码的编辑器被落在后面;更重要的是,这和 LSP 的初衷相反——解耦就是为了让服务器的每次改进都能惠及所有客户端。
提案 microsoft/language-server-protocol#1164 建立了标准机制:服务器先收集用户输入,再计算文本编辑。对 Go 开发者来说,这解锁了一批等了很久的重构:
- golang/go#70583:Move Declaration
- golang/go#38028:Change Signature
- golang/vscode-go#1547:Implement Interface
- golang/vscode-go#2002:Modify Struct Tags
增删 struct tag(gopls v0.23.0)
配置序列化格式时,自定义 tag 和命名大小写是常见需求。以前运行「Add struct tags」会无条件注入 camelCase 格式的 json tag。改成交互式之后:
- 光标放在任意 struct 定义上,选择「Add struct tags」code action。
- 弹出输入框,预填
json,可以自定义任意 tag 组合(如json,yaml,db)。 - 第二个对话框选择命名约定(如 camelCase、snake_case、PascalCase)。
- gopls 生成格式化后的 struct tag 并应用到类型定义上。
Before:
type UserProfile struct {
ID int64
FirstName string
LastName string
EmailAddr string
}
After(json,db + snake_case):
type UserProfile struct {
ID int64 `json:"id" db:"id"`
FirstName string `json:"first_name" db:"first_name"`
LastName string `json:"last_name" db:"last_name"`
EmailAddr string `json:"email_addr" db:"email_addr"`
}
对应的「Remove struct tags」现在也会问你要删哪些 tag key(比如只删 db、保留 json),而不是不加区分地把所有 struct tag 抹掉。
实现接口:带动态符号搜索
为满足某个接口而新建类型时,以前得复制接口声明再手工改写成一组接收者方法,或者借助 impl 这类外部工具——而用外部工具还得手动敲接收者、限定接口名。
自 gopls v0.23.0 起:
- 光标放在任意具体类型上,选择「Add methods to X to implement an interface…」code action。
- 出现文本输入框,让你填接口类型名。它会向 gopls 发起动态查询,补全工作区和标准库中的类型名(例如
maphash.Hasher)。 - 如果选中的接口带类型参数,gopls 会为每个类型参数询问一个具体类型实参(比如
Foo)。 - gopls 生成完整的方法桩,接收者名字和实例化后的方法签名都是对的。
结果示例:
type MyHasher struct{}
// Equal implements [maphash.Hasher].
func (m *MyHasher) Equal(x Foo, y Foo) bool {
panic("unimplemented")
}
// Hash implements [maphash.Hasher].
func (m *MyHasher) Hash(*maphash.Hash, Foo) {
panic("unimplemented")
}
跨文件、跨包移动声明(preview)
手工剪切粘贴移动声明很容易打断引用,还要在多个文件里做繁琐的 import 清理。这个功能排在后续的 gopls 版本里,但当前实现已经能演示交互式文件选择如何简化跨文件、跨包移动:
- 光标放在任意顶层声明上,选择「Move declaration X」code action。
- 文件选择对话框让你挑一个已存在的目标
.go文件,或者直接指定新文件路径。 - gopls 把声明连同它依赖的声明(未导出的辅助函数、方法等)一起搬走,无关符号留在原地;随后更新整个工作区的引用,并清理源文件与目标文件两侧的 import。
下面这个例子里,类型 Foo 从 package from 移到 package to。跟着走的是:
Foo的方法Print;- 只有
Foo用的未导出常量bar; fmt这个 import 随被移动的代码进入目标文件。
留下的是:
- 无关的函数
Baz; Baz需要的stringsimport。
也就是说,搬移的边界由「Foo 实际用到了什么」决定,而不是把整个文件里的符号和 import 一起打包。
Slay the dragon(gopls v0.24.0)
作为多步交互对话框的演示,同时兼作客户端编辑器实现的自包含测试用例,gopls v0.24.0 提供了一个「Slay the dragon」code action:把光标放在名为 dragonSlayer 的包级变量上即可触发,例如 var dragonSlayer string。
接下来要做的
- Change signature 重构(#38028):在工作区范围内增删或重排函数参数,所需的输入无法从语法推断,比如新的参数顺序、要在已有调用点插入的默认实参表达式。
- 交互式冲突解决:代码变换遇到命名冲突、标识符遮蔽、引用歧义时,gopls 可以问用户怎么处理。
- 代码编辑之外的交互式命令:交互性建模在 LSP 的 command 层,因此天然能推广到需要用户配置的非编辑类服务器操作,比如带 profiling flag(CPU、memory、tracing)地配置测试或 benchmark 运行。
上手
交互式重构在 gopls v0.24.0 和 vscode-go v0.56.0 中可用(Neovim 和 Emacs 也有探索性的概念验证客户端集成)。完整技术规范见 LSP Interactive Code Actions 设计文档与 microsoft/language-server-protocol#1164。
参考链接: