gopls 交互式重构上线:结构体标签、接口实现、移动声明都能“先问再改”

举报
Eveline 发表于 2026/10/22 20:28:36 2026/10/22
【摘要】 Go 官方博客译文:gopls v0.24.0 与 vscode-go v0.56.0 引入交互式重构,语言服务器可在改代码前向用户提问。本文介绍其背后的 LSP 提案,以及添加/删除结构体标签、实现接口、跨包移动声明等新玩法,附升级方法与译者注。

原文信息:本文译自 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 在这项工作中的见解、讨论、反馈与协作。)


译者注

  1. 怎么用上:在 VS Code 中把 Go 扩展升级到 v0.56.0 及以上,并执行 go install golang.org/x/tools/gopls@latest 把 gopls 升到 v0.24.0 及以上;在结构体或类型名上按 Ctrl+.(macOS 为 Cmd+.)即可看到对应的代码操作。
  2. 为什么值得关注:这次改动的意义不只在 Go。提案是提交给 LSP 标准本身的,一旦被采纳,Python、Rust、Java 等语言服务器都能用同一套机制实现“先提问、再改代码”的重构,Neovim、Emacs 这类轻客户端也能直接受益。
  3. 远程开发同样适用:如果你通过 VS Code Remote-SSH 连接华为云 ECS 等云服务器写 Go,gopls 运行在远端,记得在远端执行上面的升级命令。

原文:Interactive refactoring in gopls,Hongxiang Jiang,The Go Blog,2026-10-16,CC BY 4.0。

【版权声明】本文为华为云社区用户翻译文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容, 举报邮箱:cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。