编程 gopls v0.24.0 的交互式重构:服务端在算 edits 之前先向你提问

2026-11-04 20:03:26

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。现在:

  1. 光标放在 struct 定义上,选择 Add struct tags code action。
  2. 出现输入框,预填 json,可自定义任意组合(如 json,yaml,db)。
  3. 第二个对话框选择命名风格(camelCase、snake_case、PascalCase)。
  4. 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:

  1. 光标放在具体类型上,选择 Add methods to X to implement an interface…。
  2. 出现输入框,输入接口名时会向 gopls 发起动态查询,自动补全工作区与标准库里的类型(如 maphash.Hasher)。
  3. 如果接口有类型参数,gopls 会为每个类型参数询问具体类型实参(如 Foo)。
  4. 生成完整方法桩,接收者名和实例化后的方法签名都是对的。

生成的方法体默认是 panic("unimplemented"),需要自己填。

跨文件/跨包移动声明(预览)

手动剪切粘贴容易断引用,还要跨文件清理 import。该能力计划在后续 gopls 版本发布,当前实现已经能看到交互式选文件的效果:

  1. 光标放在顶层声明上,选择 Move declaration X。
  2. 文件选择器可选已有 .go 文件,或输入新文件路径。
  3. 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。

参考:

复制全文 生成海报 Go gopls LSP 重构 编辑器

推荐文章

程序员茄子在线接单