gopls v0.24.0 的交互式重构:服务端在算 edits 之前先向你提问
项目地址与文档:https://go.dev/blog/interactive-refactoring
LSP 是解耦语言特性与编辑器的开放标准,客户端与服务端通过 JSON-RPC 交换数据,提供补全、hover、诊断、code action。gopls 现在引入 interactive refactoring,基于 Go 团队为 LSP 提议的新能力,把动态、多步对话框带进编辑器。该能力与 Dart 团队合作设计,不只服务 Go,而是面向所有语言与编辑器生态。
code action 是个无参函数的现实限制
当前几乎每个 LSP code transformation 都像一个无参函数:触发操作,服务端立刻算出 edits。重命名是唯一例外——它是「一个参数」的函数(新名字),因此协议里专门有 PrepareRename RPC。
服务端只能从上下文(光标位置、所在语法树)推断意图。上下文不够时只有两个选择:套一个通用默认值,或者干脆不提供这个变换。问题不在 gopls 算不出 edits,而在 LSP 没有让服务端向用户提问的机制。
很多变换其实需要输入:加哪些 struct tag、用什么命名风格、实现哪个接口、把声明移到哪个文件。没有提问机制时 gopls 只能猜——例如 Add struct tags 永远生成 camelCase 的 json tag——或者不提供。
部分语言扩展把自定义对话框逻辑塞进单个编辑器来绕过这个限制,但这样 Neovim、Emacs 这类只按 LSP 走、几乎不写自定义客户端代码的编辑器就被落下。更根本的是,这违背了 LSP 的目的:解耦语言特性和编辑器,让服务端的一次改进惠及所有客户端。
LSP interactive refactoring 提案(microsoft/language-server-protocol#1164)建立标准机制:服务端在计算 edits 之前先收集用户输入。对 Go 开发者来说,这解锁了一批等了很久的重构:
- golang/go#70583 Move Declaration
- golang/go#38028 Change Signature
- golang/vscode-go#1547 Implement Interface
- golang/vscode-go#2002 Modify Struct Tags
可用版本:gopls v0.24.0 与官方 VS Code Go 扩展 v0.56.0。
增删 struct tag(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 之类的外部工具手动输入接收者与接口名。自 gopls v0.23.0:
- 光标放在具体类型上,选择 Add methods to X to implement an interface…。
- 出现输入框,输入接口名时会向 gopls 发起动态查询,自动补全工作区与标准库里的类型(如
maphash.Hasher)。 - 如果接口有类型参数,gopls 会为每个类型参数询问具体类型实参(如
Foo)。 - 生成完整方法桩,接收者名和实例化后的方法签名都是对的。
生成的方法体默认是 panic("unimplemented"),需要自己填。
跨文件/跨包移动声明(预览)
手动剪切粘贴容易断引用,还要跨文件清理 import。该能力计划在后续 gopls 版本发布,当前实现已经能看到交互式选文件的效果:
- 光标放在顶层声明上,选择 Move declaration X。
- 文件选择器可选已有
.go文件,或输入新文件路径。 - gopls 会连带搬走依赖它的声明(未导出的辅助函数、方法),无关符号留下,更新工作区引用,清理源文件与目标文件的 import。
例子:类型 Foo 从包 from 移到包 to,方法 Print 和只有 Foo 用的未导出常量 bar 一起搬走,无关函数 Baz 留在原地。
彩蛋
gopls v0.24.0 提供了一个 Slay the dragon code action:光标放在名为 dragonSlayer 的包级变量上(如 var dragonSlayer string)即可触发。它既是多步交互对话框的演示,也是客户端实现的独立测试用例。
路线图
- Change signature(#38028):增删、重排函数参数,需要语法之外的信息,如新的参数顺序、已有调用点插入什么默认实参表达式;交互式表单让 gopls 先收集这些再重写声明与所有调用点。
- 交互式冲突处理:遇到命名冲突、标识符遮蔽、引用歧义时向用户提问,例如是否继续,或现场重命名冲突符号。
- 编辑之外的交互命令:因为交互建模在 LSP command 层,天然可推广到需要用户配置的非编辑操作,例如带 profiling 参数(CPU、内存、trace)跑测试或 benchmark。
接入与注意
- 可用版本:gopls v0.24.0、vscode-go v0.56.0;Neovim 与 Emacs 有概念验证客户端集成,但协议提案本身还不是最终标准。
- UI 由客户端渲染,同一个功能在 VS Code 可用,不代表其他客户端已实现。
- 生成的代码仍要人看:接口桩默认
panic("unimplemented");移动声明可能改变包边界、引入 import cycle。edits 是服务端算的,安全性得人判断。 - 拿一个平时手工做的重构试一次,然后看 diff。
参考: