Gopls:代码转换功能

本文档介绍 gopls 的代码转换功能,包括一系列保持行为不变的更改(重构、格式化、简化)、代码修复(修正)以及编辑支持(填充结构体字面量和 switch 语句)。

代码转换在 LSP 中不是一个单一的类别

  • 其中一些,例如格式化和重命名,是协议中的主要操作。
  • 一些转换通过 代码提示 公开,这些代码提示返回 *命令*,即通过 workspace/executeCommand 请求调用以产生副作用的任意服务器操作;但是,目前没有代码提示是 Go 语法的转换。
  • 大多数转换被定义为 *代码操作*。

代码操作

代码操作是与文件一部分关联的操作。每次选择更改时,典型客户端都会发送一个 textDocument/codeAction 请求来获取可用操作集,然后更新其 UI 元素(菜单、图标、工具提示)以反映它们。VS Code 手册将代码操作描述为“快速修复 + 重构”。

codeAction 请求提供了菜单,但它并不点餐。一旦用户选择了一个操作,就会发生以下两种情况之一。在简单的情况下,操作本身包含一个客户端可以直接应用于文件的编辑。但在大多数情况下,该操作包含一个命令,类似于与代码提示关联的命令。这使得计算补丁的工作可以延迟进行,直到真正需要时(大多数情况下不需要)。然后服务器可以计算编辑并将 workspace/applyEdit 请求发送给客户端来修补文件。并非所有代码操作的命令都有 applyEdit 的副作用:有些可能会更改服务器状态,例如切换变量或导致服务器向客户端发送其他请求,例如 showDocument 请求以在 Web 浏览器中打开报告。

代码提示和代码操作之间的主要区别在于

  • codeLens 请求获取整个文件的命令。每个命令指定其适用的源范围,通常显示为该源范围的注解。
  • codeAction 请求仅获取特定范围(当前选区)的命令。所有命令都将一起显示在该位置的菜单中。

每个操作都有一个 *种类*,这是一个分层标识符,例如 refactor.inline.call。客户端可以根据其种类过滤操作。例如,VS Code 有:两个菜单,“重构…”和“源操作…”,每个菜单都由不同种类的代码操作(refactorsource)填充;一个灯泡图标,用于触发“快速修复”(种类为 quickfix)菜单;以及一个“全部修复”命令,用于执行所有种类为 source.fixAll 的代码操作,这些是那些被认为可以安全应用的。

Gopls 支持以下代码操作

Gopls 会报告一些代码操作两次,具有两种不同的种类,以便它们出现在多个 UI 元素中:例如,从 for _ = range m 简化为 for range m 的简化操作,其种类为 quickfixsource.fixAll,因此它们会出现在“快速修复”菜单中,并通过“全部修复”命令激活。

许多转换是由 分析器 计算的,这些分析器在报告有关问题的诊断时,还会建议修复。codeActions 请求将返回与当前选区诊断相关的任何修复。

注意事项

  • gopls 的许多代码转换受 Go 语法树表示的限制,该表示目前将注释记录在树之外的辅助表中;因此,诸如提取和内联之类的转换容易丢失注释。这是问题 https://golang.ac.cn/issue/20744,我们在 2024 年将其修复是优先事项。

  • 通过约定俗成的 DO NOT EDIT 注释标识的生成文件,不提供转换的代码操作。

客户端对代码操作的支持

  • VS Code:根据其种类,代码操作可以在“重构…”菜单(^⇧R)、“源操作…”菜单、💡(灯泡)图标菜单或“快速修复”(⌘.)菜单中找到。“全部修复”命令应用所有种类为 source.fixAll 的操作。
  • Emacs + eglot:代码操作是不可见的。使用 M-x eglot-code-actions 从可用的操作(如果存在多个)中选择一个并执行。某些操作种类具有过滤快捷方式,例如 M-x eglot-code-action-{inline,extract,rewrite}
  • CLIgopls codeaction -exec -kind k,... -diff file.go:#123-#456 对选定的范围(使用从零开始的字节偏移指定)执行指定种类的代码操作(例如 refactor.inline),并显示差异。

格式化

LSP textDocument/formatting 请求返回格式化文件的编辑。Gopls 应用 Go 的标准格式化算法,go fmt。LSP 格式化选项被忽略。

大多数客户端配置为在文件保存时格式化文件和组织导入。

设置

客户端支持

  • VS Code:默认在保存时格式化。使用“格式化文档”菜单项(⌥⇧F)手动调用。
  • Emacs + eglot:使用 M-x eglot-format-buffer 进行格式化。将其附加到 before-save-hook 以在保存时格式化。对于格式化与 organize-imports 结合使用,许多用户采用传统方法,将 "goimports" 设置为 gofmt-command(使用 go-mode),并将 gofmt-before-save 添加到 before-save-hook。基于 LSP 的解决方案需要类似 https://github.com/joaotavora/eglot/discussions/1409 的代码。
  • CLIgopls format file.go

source.organizeImports:组织导入

在导入未组织的文件的 codeActions 请求将返回一个标准种类为 source.organizeImports 的操作。其命令的作用是组织导入:删除重复或未使用的现有导入,为未定义的符号添加新导入,并按约定顺序排序。

添加新导入是基于启发式方法,这些方法取决于您的工作区和 GOMODCACHE 目录的内容;有时它们可能会做出令人惊讶的选择。

许多编辑器会在保存已编辑的文件之前自动组织导入并格式化代码。

一些用户不喜欢自动删除未引用的导入,因为例如,唯一引用该导入的一行被临时注释掉了以进行调试;请参阅 https://golang.ac.cn/issue/54362

设置

  • local 设置是以逗号分隔的导入路径前缀列表,这些前缀对于当前文件是“本地的”,并且在排序顺序中应出现在标准和第三方包之后。

客户端支持

  • VS Code:默认在保存前自动调用 source.organizeImports。要禁用它,请使用以下代码片段,并根据需要手动调用“组织导入”命令。
    "[go]": {
      "editor.codeActionsOnSave": { "source.organizeImports": false }
    }
    
  • Emacs + eglot:使用 M-x eglot-code-action-organize-imports 手动调用。许多 go-mode 用户使用以下几行代码在保存每个修改过的文件之前组织导入并重新格式化,但这种方法是基于旧版 goimports 工具,而不是 gopls。
    (setq gofmt-command "goimports")
    (add-hook 'before-save-hook 'gofmt-before-save)
    
  • CLIgopls fix -a file.go:#offset source.organizeImports

source.addTest:为函数或方法添加测试

如果选定的代码块是函数或方法声明 F 的一部分,gopls 将提供“为 F 添加测试”代码操作,该操作将在相应的 _test.go 文件中为选定的函数添加一个新测试。生成的测试会考虑其签名,包括输入参数和结果。

测试文件:如果 _test.go 文件不存在,gopls 会根据当前文件名(a.go -> a_test.go)创建它,并从原始文件中复制任何版权和构建约束注释。

测试包:对于测试包 p 中代码的新文件,测试文件尽可能使用 p_test 包名,以鼓励仅测试导出的函数。(如果测试文件已存在,则将新测试添加到该文件中。)

参数:函数中每个非空参数都成为表格驱动测试使用的结构体中的一个字段。(对于每个空 _ 参数,值没有影响,因此测试提供零值参数。)

上下文:如果第一个参数是 context.Context,测试将传递 context.Background()

结果:函数的返回值被赋给变量(gotgot2 等),并与测试用例结构体中定义的预期值(wantwant2 等)进行比较。用户应编辑逻辑以执行适当的比较。如果最终结果是 error,则测试用例会定义一个 wantErr 布尔值。

方法接收者:测试方法 T.F(*T).F 时,测试必须构造一个 T 的实例作为接收者。Gopls 会在包中搜索一个合适的函数,该函数构造一个类型为 T 或 *T 的值,可选地带有一个错误,并优先选择名为 NewT 的函数。

导入:Gopls 会添加测试文件中缺失的导入,使用原始文件中最后一个相应的导入说明符。它避免重复导入,并保留测试文件中已有的导入。

重命名

LSP textDocument/rename 请求用于重命名符号。

重命名是一个两阶段过程。第一阶段是 prepareRename 查询,它返回光标下的标识符的当前名称(如果存在)。然后客户端显示一个对话框,提示用户通过编辑旧名称来选择新名称。第二阶段,即真正的 rename,应用更改。(这种简单的对话框支持在 LSP 重构操作中是独一无二的;请参阅 microsoft/language-server-protocol#1164。)

Gopls 的重命名算法非常仔细地检测重命名可能导致编译错误的场景。例如,更改名称可能会导致符号被“遮蔽”,从而使某些现有引用不再在作用域内。Gopls 将报告一个错误,说明符号对和被遮蔽的引用。

再举个例子,考虑重命名一个具体类型的方​​法。重命名可能会导致该类型不再满足与之前相同的接口,这可能导致程序无法编译。为避免这种情况,gopls 会检查从受影响类型到接口类型的每个转换(显式或隐式),并检查在重命名后该转换是否仍然有效。如果无效,它将中止重命名并报告错误。

如果您打算重命名原始方法以及任何匹配接口类型的相应方法(以及与之对应的类型的任何方法),则可以通过在接口方法上调用重命名操作来指示这一点。

同样,如果您重命名了一个嵌入了某个类型的匿名结构体字段,gopls 将报告一个错误,因为这需要一个涉及该类型的更大范围的重命名。如果您意图这样做,您可以通过在类型上调用重命名操作来指示这一点。

重命名永远不应导致编译错误,但可能会导致运行时错误。例如,在方法重命名中,如果受影响类型到接口类型没有直接转换,但有一个到更宽泛类型的中间转换(例如 any),然后进行到接口类型的类型断言,那么 gopls 可能会继续重命名方法,导致类型断言在运行时失败。使用反射的包,例如 encoding/jsontext/template,可能会出现类似的问题。没有捷径可取代良好的判断和测试。

特殊情况

  • 重命名方法接收者声明时,该工具还会尝试重命名同一命名类型的所有其他方法的接收者。无法完全重命名的其他每个接收者将被静默跳过。重命名任何接收者的 *使用* 只会影响该变量。

    type Counter struct { x int }
    
                     Rename here to affect only this method
                              ↓
    func (c *Counter) Inc() { c.x++ }
    func (c *Counter) Dec() { c.x++ }
          ↑
      Rename here to affect all methods
    
  • 重命名包声明还会导致重命名包的目录。

最佳结果的一些技巧

  • 重命名算法执行的安全检查需要类型信息。如果程序严重格式错误,可能缺乏足够的信息使其运行(https://golang.ac.cn/issue/41870),并且重命名通常不能用于修复类型错误(https://golang.ac.cn/issue/41851)。在重构时,我们建议分小步进行,并在进行过程中修复任何问题,以便在每一步尽可能多地编译程序。
  • 有时,重命名操作改变程序的引用结构是有益的,例如通过将 y 重命名为 x 来有意组合两个变量 xy。重命名工具过于严格,无法在此情况下提供帮助(https://golang.ac.cn/issue/41852)

有关 gopls 重命名算法的详细信息,您可能对 2015 年 GothamGo 会议的后半部分感兴趣:使用 go/types 进行代码理解和重构工具

客户端支持

  • VS Code:使用“重命名符号”菜单项(F2)。
  • Emacs + eglot:使用 M-x eglot-rename,或 go-mode 中的 M-x go-rename
  • Vim + coc.nvim:使用 coc-rename 命令。
  • CLIgopls rename file.go:#offset newname

refactor.extract:提取函数/方法/变量

refactor.extract 系列代码操作都返回命令,这些命令用新创建的包含选定代码的声明的引用替换选定的表达式或语句。

  • refactor.extract.function 用对名为 newFunction 的新函数的调用替换一个或多个完整语句,该新函数体包含这些语句。选区必须包含的语句少于现有函数的整个体。

    Before extracting a function After extracting a function

  • refactor.extract.method 是“提取函数”的一个变体,当选定的语句属于某个方法时提供。新创建的函数将是同一接收者类型的方​​法。

  • refactor.extract.variable 用对名为 newVar 的新局部变量的引用替换表达式,该局部变量由该表达式初始化。

    Before extracting a var After extracting a var

  • `refactor.extract.constant` 对常量表达式执行相同的操作,引入一个局部 const 声明。

  • refactor.extract.variable-all 将函数中选定表达式的所有出现都替换为对名为 newVar 的新局部变量的引用。这会提取一次表达式并在函数中任何出现的地方重复使用它。

    Before extracting all occurrences of EXPR After extracting all occurrences of EXPR

    • `refactor.extract.constant-all` 对常量表达式执行相同的操作,引入一个局部 const 声明。如果新声明的默认名称已被使用,gopls 会生成一个新名称。

提取是一个具有挑战性的问题,需要考虑标识符作用域和遮蔽、控制流(例如循环中的 break/continue 或函数中的 return)、变量的基数,甚至细微的风格问题。在每种情况下,该工具都会尝试根据需要更新提取的语句,以避免构建中断或行为更改。不幸的是,gopls 的提取算法远不如重命名和内联操作严谨,并且我们注意到一些它表现不佳的情况,包括:

以下是计划在 2024 年支持但尚未支持的提取功能:

refactor.extract.toNewFile:将声明提取到新文件

(gopls/v0.17.0 起可用)

如果您选择一个或多个顶层声明,gopls 将提供“将声明提取到新文件”代码操作,该操作会将选定的声明移动到一个新文件中,该文件的名称基于第一个声明的符号。根据需要创建导入声明。Gopls 还会在选区只是声明的第一个标记(例如 functype)时提供此代码操作。

Before: select the declarations to move After: the new file is based on the first symbol name

refactor.inline.call:内联函数调用

对于 codeActions 请求,如果选区是(或位于)函数或方法的调用中,gopls 将返回一个种类为 refactor.inline.call 的命令,该命令的作用是内联函数调用。

下面的截图显示了在内联之前和之后对 sum 的调用。

Before: select Refactor… Inline call to sum After: the call has been replaced by the sum logic

内联会将调用表达式替换为函数体的副本,并将参数替换为实参。内联有很多好处。也许您想通过用较新的 os.ReadFile 替换对已弃用函数(如 ioutil.ReadFile)的调用;内联会为您做到这一点。或者您可能想复制并修改现有函数;内联可以提供一个起点。内联逻辑还为其他重构(例如“更改签名”)提供了构建块。

并非所有调用都可以内联。当然,该工具需要知道正在调用哪个函数,因此您无法通过函数值或接口方法进行动态调用,但静态调用方法是没问题的。如果被调用方声明在另一个包中,并且引用了该包的非导出部分,或者引用了调用者无法访问的 内部包,则也不能内联。泛型函数的调用尚不支持(https://golang.ac.cn/issue/63352),但我们计划修复它。

在可以内联的情况下,工具必须保留程序的原始行为至关重要。我们不希望重构破坏构建,或者更糟的是,引入微妙的潜在错误。这在使用内联工具进行大规模代码库的自动化清理时尤其重要,我们必须能够信任该工具。我们的内联器非常谨慎,不会对代码行为进行猜测或做出不合理的假设。然而,这意味着它有时会产生与具有相同代码的专家手动编写的内容不同的更改。

在最困难的情况下,尤其是在复杂的控制流中,完全消除函数调用可能不安全。例如,defer 语句的行为与其封闭的函数调用密切相关,而 defer 是唯一可用于处理恐慌的控制结构,因此它不能简化为更简单的结构。因此,例如,给定一个定义为

func f(s string) {
    defer fmt.Println("goodbye")
    fmt.Println(s)
}

f("hello") 的调用将被内联为

    func() {
        defer fmt.Println("goodbye")
        fmt.Println("hello")
    }()

尽管参数被消除了,但函数调用仍然保留。

内联器有点像优化编译器。编译器被认为是“正确”的,如果它在从源语言到目标语言的翻译过程中不改变程序的含义。*优化*编译器利用输入的特定情况来生成更好的代码,其中“更好”通常意味着更有效。随着用户报告导致编译器发出次优代码的输入,编译器会得到改进,以识别更多情况、规则或规则的例外情况——但这个过程没有尽头。内联是类似的,不同之处在于“更好”的代码意味着更整洁的代码。最保守的翻译提供了一个简单但(希望)正确的底层,在此之上,无尽的规则和规则的例外可以美化和提高输出质量。

以下是一些涉及健全内联的技术挑战:

  • 副作用:用参数表达式替换参数时,我们必须小心不要改变调用的副作用。例如,如果我们调用一个函数 func twice(x int) int { return x + x } 并使用 twice(g()),我们不希望看到 g() + g(),这会导致 g() 的副作用发生两次,并且每次调用可能返回不同的值。所有副作用必须发生相同的次数,并以相同的顺序。这需要分析参数和被调用函数,以确定它们是否是“纯”的,它们是否读取变量,或者它们是否(以及何时)更新它们。当无法证明代换参数是安全的时,内联器将引入一个像 var x int = g() 这样的声明。

  • 常量:如果内联总是将参数替换为其常量值,则某些程序将不再构建,因为之前在运行时进行的检查将在编译时进行。例如 func index(s string, i int) byte { return s[i] } 是一个有效函数,但如果内联将调用 index("abc", 3) 替换为表达式 "abc"[3],编译器将报告索引 3 超出字符串 "abc" 的边界。内联器将阻止代换参数为有问题的常量参数,同样引入一个 var 声明。

  • 引用完整性:当参数变量被替换为其参数表达式时,我们必须确保参数表达式中的任何名称继续引用相同的东西——而不是指向被调用函数体中碰巧使用相同名称的不同声明。内联器必须将局部引用(如 Printf)替换为限定引用(如 fmt.Printf),并根据需要添加 fmt 包的导入。

  • 隐式转换:将参数传递给函数时,它会被隐式转换为参数类型。如果我们消除了参数变量,我们不希望丢失转换,因为它可能很重要。例如,在 func f(x any) { y := x; fmt.Printf("%T", &y) } 中,变量 y 的类型是 any,因此程序打印 "*interface{}"。但是,如果内联调用 f(1) 会产生语句 y := 1,那么 y 的类型将变为 int,这可能会导致编译错误或,如本例所示,导致错误,因为程序现在打印 "*int"。当内联器用参数值代换参数变量时,它可能需要引入每个值到原始参数类型的显式转换,例如 y := any(1)

  • 最后引用:当参数表达式没有副作用且其对应的参数从未被使用时,可以消除该表达式。但是,如果表达式包含调用方中局部变量的最后一次引用,这可能会导致编译错误,因为该变量现在未被使用。因此,内联器在消除对局部变量的引用时必须谨慎。

这只是问题领域的一瞥。如果您有兴趣,golang.org/x/tools/internal/refactor/inline 的文档提供了更多详细信息。所有这一切都说明,这是一个复杂的问题,我们首先以正确性为目标。我们已经实现了一些重要的“整洁优化”,并预计未来会有更多。

refactor.inline.variable:内联局部变量

对于 codeActions 请求,如果选区是(或位于)一个局部变量的用法标识符,并且该局部变量的声明具有初始化表达式,gopls 将返回一个种类为 refactor.inline.variable 的代码操作,其作用是内联变量:即,将引用替换为变量的初始化表达式。

例如,如果在调用 println(s) 上的标识符 s 处调用

func f(x int) {
    s := fmt.Sprintf("+%d", x)
    println(s)
}

代码操作将代码转换为

func f(x int) {
    s := fmt.Sprintf("+%d", x)
    println(fmt.Sprintf("+%d", x))
}

(在这种情况下,s 成为了一个未被引用的变量,您需要将其删除。)

即使有后续对变量的赋值(例如 s = ""),该代码操作也始终用初始化表达式替换引用。

如果无法进行转换,因为初始化表达式中的一个标识符(例如上面示例中的 x)被一个中间声明所遮蔽,则代码操作将报告错误,如下例所示:

func f(x int) {
    s := fmt.Sprintf("+%d", x)
    {
        x := 123
        println(s, x) // error: cannot replace s with fmt.Sprintf(...) since x is shadowed
    }
}

refactor.rewrite:杂项重写

本节涵盖了一些可作为代码操作访问的转换,其种类是 refactor.rewrite 的子项。

refactor.rewrite.removeUnusedParam:删除未使用的参数

unusedparams 分析器 为函数体中未使用的每个参数报告一个诊断。例如:

func f(x, y int) { // "unused parameter: x"
    fmt.Println(y)
}

它 *不* 为地址被取用的函数报告诊断,这些函数可能需要所有参数(即使是未使用的)才能符合特定的函数签名。它也不为导出的函数报告诊断,因为导出函数可能被另一个包取用地址。(如果一个函数被 *取用地址*,则意味着它除了在调用位置 f(...) 之外也被使用了。)

除了诊断之外,它还建议两种可能的修复:

  1. 将参数重命名为 _,以强调其未被引用(即时编辑);或者
  2. 使用 ChangeSignature 命令完全删除参数,并更新所有调用方。

修复 #2 使用与“内联函数调用”(见上文)相同的机制,以确保所有现有调用的行为都得到保留,即使被删除参数的参数表达式具有副作用,如下例所示。

The parameter x is unused The parameter x has been deleted

请注意,在第一个调用中,参数 chargeCreditCard() 因潜在的副作用而未被删除,而在第二个调用中,参数 2(一个常量)被安全地删除了。

refactor.rewrite.moveParam{Left,Right}:移动函数参数

当选定内容是函数或方法签名中的参数时,gopls 提供一个代码操作来(如果可能)将参数向左或向右移动,并相应地更新所有调用方。

例如

func Foo(x, y int) int {
    return x + y
}

func _() {
    _ = Foo(0, 1)
}

变为

func Foo(y, x int) int {
    return x + y
}

func _() {
    _ = Foo(1, 0)
}

在请求将 x 向右移动或将 y 向左移动之后。

这是更通用的“更改签名”操作的一个基本构建块。我们计划将其泛化为任意签名重写,但语言服务器协议目前不支持用户输入重构操作(请参阅 microsoft/language-server-protocol#1164)。因此,任何此类重构都需要自定义客户端逻辑。(作为一个非常粗糙的解决方法,您可以通过在函数声明的 func 关键字上调用重命名来表达任意参数移动,但此接口只是一个暂时的权宜之计。)

refactor.rewrite.changeQuote:在原始和解释的字符串字面量之间转换

当选定内容是一个字符串字面量时,gopls 提供一个代码操作,用于在可能的情况下将字符串在原始形式(`abc`)和解释形式("abc")之间转换。

Convert to interpreted Convert to raw

第二次应用该代码操作可恢复到原始形式。

refactor.rewrite.invertIf:反转“if”条件

当选定内容在 if/else 语句内,并且后面没有 else if 时,gopls 提供一个代码操作来反转该语句,否定条件并交换 ifelse 块。

Before “Invert if condition” After “Invert if condition”

refactor.rewrite.{split,join}Lines:将元素拆分成单独的行

当选定内容在方括号列表项内时,例如

  • 复合字面量的*元素*,[]T{a, b, c}
  • 函数调用的*参数*,f(a, b, c)
  • 函数签名的*参数组*,func(a, b, c int, d, e bool),或
  • 其*结果组*,func() (x, y string, z rune)

gopls 将提供“将[项]拆分成单独的行”代码操作,这将把上述形式转换为这些形式。

[]T{
    a,
    b,
    c,
}

f(
    a,
    b,
    c,
)

func(
    a, b, c int,
    d, e bool,
)

func() (
    x, y string,
    z rune,
)

请注意,在最后两种情况下,每个参数或结果的*组*被视为一个单独的项。

相反的代码操作“将[项]合并为一行”可以撤销该操作。如果列表已经完全拆分或合并,或者列表很简单(少于两个项),则不会提供任一操作。

对于包含 // 风格注释的列表,这些注释会延续到行尾,因此不会提供这些代码操作。

refactor.rewrite.fillStruct:填充结构体字面量

当光标位于结构体字面量 S{} 内时,gopls 提供“填充 S”代码操作,该操作会填充字面量中所有可访问的缺失字段。

它使用以下启发式方法来选择分配给每个字段的值:它查找可分配给该字段的候选变量、常量和函数,并选择名称与字段名称最匹配的。如果没有,它将使用字段类型的零值(例如 0""nil)。

在下面的示例中,一个 slog.HandlerOptions 结构体字面量使用两个局部变量(leveladd)以及一个函数(replace)进行填充。

Before “Fill slog.HandlerOptions” After “Fill slog.HandlerOptions”

注意事项

  • 此代码操作需要结构体类型的类型信息,因此如果它定义在尚未导入的另一个包中,您可能需要先“组织导入”,例如通过保存文件。
  • 仅在当前文件中,并且仅在当前点之上搜索候选声明。声明在当前点下方或包中其他文件的符号不被考虑;请参阅 https://golang.ac.cn/issue/68224

refactor.rewrite.fillSwitch:填充 switch

当光标位于 switch 语句内,且其操作数类型是 *枚举*(一组命名的常量)或在类型 switch 中时,gopls 将提供“为 T 添加 case”代码操作,该操作通过为枚举类型的每个可访问命名常量添加一个 case,或对于类型 switch,通过为每个可访问的非接口类型(实现该接口)添加一个 case 来填充 switch 语句。仅添加缺失的 case。

下面的截图显示了一个类型 switch,其操作数具有 net.Addr 接口类型。代码操作为每种具体的网络地址类型添加一个 case,加上一个默认 case,该 case 会在遇到意外操作数时以信息性消息 panic。

Before “Add cases for Addr” After “Add cases for Addr”

这些截图展示了代码操作为 html.TokenType 枚举类型的每个值添加 case,该枚举类型表示 HTML 文档组成的各种标记类型。

Before “Add cases for Addr” After “Add cases for Addr”

refactor.rewrite.eliminateDotImport:消除点导入

当光标位于点导入上时,gopls 可以提供“消除点导入”代码操作,该操作会从导入中删除点,并在整个文件中限定包的使用。此代码操作仅在每个包的使用都可以限定而不会与现有名称发生冲突时提供。

refactor.rewrite.addTags:添加结构体标签

当光标位于结构体内部时,此代码操作会为每个字段添加一个 json 结构体标签,该标签指定其 JSON 名称,使用小写并带下划线(例如 LinkTarget 变为 link_target)。对于高亮显示的选区,它仅为选定的字段添加标签。

refactor.rewrite.removeTags:删除结构体标签

当光标位于结构体内部时,此代码操作会清除所有结构体字段上的结构体标签。对于高亮显示的选区,它仅为选定的字段移除标签。


本文档的源代码可以在 golang.org/x/tools/gopls/doc 下找到。