gopls v0.24.0 交互式重构:结构体标签、接口实现与跨文件移动都能先问再做
gopls 加入了交互式重构(interactive refactoring)。它依赖 LSP 的一项新提案:把动态、多步的对话框直接搬进编辑器,让语言服务器在计算文本编辑之前先收集用户输入。该能力与 Dart 团队合作设计,目标是成为面向所有语言和编辑器的开放标准。gopls v0.24.0 和 VS Code 官方 Go 扩展 v0.56.0 已提供可用的支持。
来源:The Go Blog,2026 年 10 月 16 日,Hongxiang Jiang。
LSP 现有的重构模型
LSP 是一个开放标准,把语言相关功能与编辑器解耦。编辑器和语言服务器通过 JSON-RPC 交换消息,提供自动补全、悬停文档、诊断和 code action 等能力。
在 LSP 里,几乎每一种代码转换都像一个无参函数:触发操作,服务器立刻算出编辑并返回。Rename 是唯一的例外——它是接受一个参数(新名字)的函数,因此需要单独的 PrepareRename RPC。
以“Add struct tags”为例,服务器只能从上下文推断意图:光标位置和所在语法树。上下文不够时,要么套一个统一默认值,要么干脆不提供这个转换。限制不在 gopls 计算编辑的能力,而在 LSP 缺少向用户提问的机制。没法问,gopls 只能猜——“Add struct tags”过去永远生成 camelCase 的 json tag。
部分语言扩展曾在各个编辑器里内嵌自定义的对话框逻辑来绕过这一点,代价是 Neovim、Emacs 这类严格遵循 LSP、只写少量客户端代码的编辑器被落在后面。按编辑器各做一套 workaround,与 LSP 的初衷相悖。
LSP 交互式重构提案
提案 microsoft/language-server-protocol#1164 定义了标准机制:语言服务器在计算文本编辑之前先收集用户输入。
由此解锁的重构包括:
- golang/go#70583 Move Declaration
- golang/go#38028 Change Signature
- golang/vscode-go#1547 Implement Interface
- golang/vscode-go#2002 Modify Struct Tags
可以把它理解为一份交互式重构的白名单:只有登记在册的 code action 走对话框流程,其余转换仍保持原来的一次性计算路径。目前走这条路径的是结构体标签的增删、接口实现、移动声明,以及用于演示的 Slay the dragon。
增删结构体标签(gopls v0.23.0)
过去“Add struct tags”无条件注入 camelCase 的 json tag。改为交互式之后:
- 光标放在 struct 上,选择“Add struct tags” code action;
- 弹出输入框,预填
json,可改成任意组合,例如json,yaml,db; - 第二个对话框选择命名风格:camelCase、snake_case、PascalCase;
- gopls 应用格式化后的 tag。
改动前:
type UserProfile struct {
ID int64
FirstName string
LastName string
EmailAddr string
}
改动后:
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。
实现接口:动态符号搜索
以前要用两种方式实现接口:复制接口声明,手工改写成方法;或者用外部工具 impl,手动输入 receiver 和限定后的接口名。
gopls v0.23.0 起:光标放在具体类型上,选择“Add methods to X to implement an interface…”,出现一个文本输入框要求填写接口类型名。输入过程中,gopls 动态查询工作区和标准库,自动补全候选名,例如 maphash.Hasher。若接口带类型参数,gopls 会逐个询问对应的具体类型实参(例如 Foo)。最后生成完整的方法 stub,包含 receiver 名和已实例化的签名。
生成结果示例:
// 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") }
跨文件、跨包移动声明(预览)
手工移动声明会打断引用,还得清理 import。该功能计划在后续 gopls 版本发布,当前实现已经能演示交互式选择文件的过程:
- 光标放在顶层声明上,选择“Move declaration X”;
- 文件选择器里挑一个已有的
.go文件,或输入新文件路径; - gopls 连同依赖声明一起移动——未导出的 helper、方法都会跟随;
- 更新整个工作区的引用,清理源文件和目标文件的 import。
示例:把 type Foo 从 package from 移到 package to,它的方法 Print 和未导出常量 bar 一起搬走,无关的 func Baz 留在原处。
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 层,因此可以推广到需要用户配置的非编辑操作,比如为 test 或 benchmark 运行配置 profiling 标志(CPU、memory、tracing)。
获取与反馈
功能已在 gopls v0.24.0 和 vscode-go v0.56.0 中提供;Neovim 和 Emacs 做过概念验证性质的客户端集成。
反馈可提交到 Go issue tracker,也可参考 LSP Interactive Code Actions 设计文档(覆盖无状态表单协议的设计与备选方案)以及提案 microsoft/language-server-protocol#1164。