gopls 交互式重构上线:结构体标签、接口实现、移动声明都能“先问再改”
原文信息:本文译自 Go 官方博客《Interactive refactoring in gopls》,作者 Hongxiang Jiang,发布于 2026 年 10 月 16 日。原文链接:https://go.dev/blog/interactive-refactoring 。原文采用 CC BY 4.0 许可,代码采用 BSD 许可。本文为翻译,译者在文末补充了译者注;原文配图请见原文链接。
译者导语:用 VS Code 写 Go 的同学大概都遇到过这种情况:点一下“Add struct tags”,结果只能生成 json 标签、而且固定是驼峰命名。原因不在 gopls,而在 LSP(语言服务器协议)本身——服务器没办法在重构过程中“问用户一句”。gopls v0.24.0 带来的交互式重构(interactive refactoring)补上了这块短板,下面是 Go 团队的官方介绍。
在现代 IDE 或编辑器里写代码时,开发者依赖重构功能来自动完成常见的代码变换。今天,我们正式在 gopls 中推出交互式重构。它基于我们为 语言服务器协议(Language Server Protocol,LSP) 提出的一项新能力,让动态的、多步骤的对话框直接出现在你常用的编辑器里。关键在于,这项能力(与 Dart 团队合作设计)并不只为 Go 服务,而是一个面向所有编程语言和编辑器生态的开放标准。gopls v0.24.0 和 VS Code 官方 Go 扩展 v0.56.0 已经提供了可用的支持。
什么是语言服务器协议(LSP)?
LSP 是一个开放标准,它把语言相关的功能从具体的文本编辑器中解耦出来。你在浏览和编辑代码时,编辑器(如 VS Code、Neovim、Emacs)和语言服务器(如 gopls)一直通过 JSON-RPC 通信,由此实现自动补全、悬停文档、诊断信息和代码操作(code action)等功能。
多步骤的代码变换
目前,LSP 里几乎所有代码变换都像一个“没有参数的函数”:你触发操作,服务器立刻算出要做的修改。(重命名符号是唯一的例外:它是带一个参数——新名字——的函数,因此协议里专门为它设计了 PrepareRename 这个 RPC。)
(原文配图:“Add struct tags”,不提问的版本)
语言服务器只能凭上下文——也就是光标位置和外层语法树——去推断用户意图。上下文不够时,服务器只有两条路:套用一个“一刀切”的默认值,或者干脆不提供这个变换。问题不在于 gopls 算不出所需的修改,而在于 LSP 缺少一种让语言服务器向用户索要输入的机制。
很多变换用这种无参数模型就够了,但也有很多需要用户额外输入:要加哪些结构体标签、用哪种命名风格,要实现哪个接口,要把声明移到哪个文件。没办法提问时,gopls 只能靠猜——比如“Add struct tags”永远生成 camelCase 的 json 标签——或者干脆不提供这项重构。有了交互式重构,语言服务器就能轻松提供带一个或多个参数的新操作。
(原文配图:“Add struct tags”,会提问的版本)
有些语言扩展为了绕开这一限制,把自定义的、语言专属的对话框逻辑直接塞进了某个编辑器里。但这样一来,那些严格遵循 LSP、客户端代码尽量精简的编辑器(如 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)
还有很多其他功能。
交互式重构实战
添加和删除结构体标签(gopls v0.23.0)
配置序列化格式时,开发者经常需要自定义标签和大小写约定。以前,“Add struct tags”会无条件地插入 camelCase 格式的 json 标签。
有了交互式重构之后:
- 把光标放在任意结构体定义上,选择 “Add struct tags” 代码操作。
- 弹出一个输入框,默认填好了
json,你可以改成任意标签组合(比如json,yaml,db)。 - 第二个对话框让你选择命名风格(比如
camelCase、snake_case或PascalCase)。 - gopls 生成格式化好的结构体标签,并应用到整个类型定义上。
变换前后对比如下:
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” 现在也会先问你要删除哪些标签键(比如只删 db 标签、保留 json 标签),而不是一股脑清空所有结构体标签。
通过动态符号搜索实现接口
当你新建一个类型去满足某个接口时,过去得把接口声明复制过来,再手动改成接收者类型上的一组方法(或者借助 impl 这类外部工具,手动输入接收者并写出接口的完整限定名)。
从 gopls v0.23.0 起,有了交互式重构:
- 把光标放在任意具体类型上,选择 “Add methods to X to implement an interface…” 代码操作。
- 出现一个文本输入框,让你选择接口类型的名字。它会动态查询 gopls,自动补全工作区和标准库中的类型名(比如
maphash.Hasher)。 - 如果选中的接口带类型参数,gopls 会为每个类型参数询问一个具体的类型实参(比如
Foo)。 - gopls 生成完整的方法桩代码,带上合适的接收者名称和实例化后的方法签名。
type Foo struct{}
type MyHasher struct{}
type Foo struct{}
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")
}
跨文件、跨包移动声明(预览)
靠剪切粘贴手动移动声明,很容易把引用弄坏,还得在多个文件里繁琐地清理 import。这项功能计划在即将发布的 gopls 版本中上线,我们已有的实现已经能展示交互式文件选择如何简化跨文件、跨包的移动:
- 把光标放在任意顶层声明上,选择 “Move declaration X” 代码操作。
- 文件选择对话框让你挑一个已有的目标
.go文件,或者指定一个新文件路径。 - gopls 会把该声明连同它依赖的声明(比如未导出的辅助函数或方法)一起移走,同时保留无关的符号,更新整个工作区的引用,并清理源文件和目标文件里的 import。
下面的例子中,我们把类型 Foo 从包 from 移到包 to。它的方法 Print,以及只有 Foo 用到的未导出常量 bar,会跟着一起移动;而无关的函数 Baz 留在原地:
// from/from.go
package from
import (
"fmt"
"strings"
)
const bar = "bar"
// Doc comment for Foo
type Foo struct{}
func (f Foo) Print() {
fmt.Println(bar)
}
func Baz() string {
return strings.ToUpper("baz")
}
// from/from.go
package from
import "strings"
func Baz() string {
return strings.ToUpper("baz")
}
// to/to.go
package to
import "fmt"
const bar = "bar"
// Doc comment for Foo
type Foo struct{}
func (f Foo) Print() {
fmt.Println(bar)
}
彩蛋:“Slay the dragon”演示操作(gopls v0.24.0)
gopls v0.24.0 还藏了一个小彩蛋,专门用来演示多步交互对话框,也方便编辑器开发者拿来测试自己的客户端实现:声明一个名为 dragonSlayer 的包级变量(比如 var dragonSlayer string),把光标放在上面,就会出现名为 “Slay the dragon”(屠龙)的代码操作。感兴趣的话不妨亲手试一试。
接下来的方向
交互式重构是 gopls 的一项基础能力,也让我们的路线图上多了几个值得期待的方向:
- 修改函数签名重构(#38028):在整个工作区增加、删除或重排函数参数,需要一些无法仅从语法推断的输入,比如新的参数顺序,或者要在已有调用处插入的默认实参表达式。交互式表单让 gopls 可以先收集这些信息,再改写声明和所有调用处。
- 交互式冲突解决:当代码变换遇到命名冲突、标识符遮蔽或引用歧义时,gopls 可以征求用户意见,比如询问是否继续,或者提议当场重命名冲突的符号。
- 代码编辑之外的交互式命令:由于交互能力建模在 LSP 的 command 层,它自然可以推广到不涉及编辑、但需要用户配置的服务器操作,比如带性能分析参数(CPU、内存、tracing)运行测试或基准测试。
上手与反馈
交互式重构已在 gopls v0.24.0 和 vscode-go v0.56.0 中提供(Neovim 和 Emacs 也已有概念验证性质的客户端集成探索)。
我们很想听听你的想法,帮助我们继续打磨 gopls 的交互式代码操作:
- 一般反馈或功能需求,请在 Go issue tracker 提 issue。
- 想阅读完整的技术规范——包括无状态表单协议的设计和我们探索过的其他方案——或者加入更广泛的 LSP 社区讨论,请查看 LSP 交互式代码操作设计文档 以及 microsoft/language-server-protocol#1164 中的讨论。
(特别感谢 Alan Donovan、Alex Putman、Brian Wilkerson、Danny Tuppeny、Hana Kim、Madeline Kalil、Peter Weinberger 和 Robert Findley 在这项工作中的见解、讨论、反馈与协作。)
译者注
- 怎么用上:在 VS Code 中把 Go 扩展升级到 v0.56.0 及以上,并执行
go install golang.org/x/tools/gopls@latest把 gopls 升到 v0.24.0 及以上;在结构体或类型名上按Ctrl+.(macOS 为Cmd+.)即可看到对应的代码操作。 - 为什么值得关注:这次改动的意义不只在 Go。提案是提交给 LSP 标准本身的,一旦被采纳,Python、Rust、Java 等语言服务器都能用同一套机制实现“先提问、再改代码”的重构,Neovim、Emacs 这类轻客户端也能直接受益。
- 远程开发同样适用:如果你通过 VS Code Remote-SSH 连接华为云 ECS 等云服务器写 Go,gopls 运行在远端,记得在远端执行上面的升级命令。
原文:Interactive refactoring in gopls,Hongxiang Jiang,The Go Blog,2026-10-16,CC BY 4.0。
- 点赞
- 收藏
- 关注作者
评论(0)