Gopls:诊断

Gopls 会不断地为所有打开的源代码文件提供各种诊断信息。每次编辑文件或更改配置时,gopls 都会异步重新计算这些诊断信息,并通过 LSP publishDiagnostics 通知发送给客户端,从而提供实时反馈,减少常见错误的成本。

诊断信息主要来自两个来源:编译错误和分析发现。

  • 编译错误 是您运行 go build 时会遇到的错误。Gopls 实际上并不运行编译器;这会太慢。相反,它会在(需要时)运行 go list 来计算编译元数据,然后以类似于编译器前端的方式处理这些包:读取、扫描和解析源文件,然后进行类型检查。这些步骤中的每一步都可能产生 gopls 会作为诊断信息显示的错误。

    LSP Diagnostic 记录的 source 字段指出了诊断信息的来源:值为 "go list" 的诊断信息来自 go list 命令,值为 "compiler" 的诊断信息来自 gopls 的解析或类型检查阶段,这些阶段与 Go 编译器使用的阶段类似。

    A diagnostic due to a type error

    上面的示例显示了 string + int 的加法,导致类型检查器报告 MismatchedTypes 错误。该诊断信息包含指向有关此类类型错误的文档的链接。

  • 分析发现 来自 Go 分析框架,该框架是 go vet 用于对 Go 代码应用各种附加静态检查的系统。最广为人知的例子是 printf 分析器,它会报告 fmt.Printf 的调用,其中格式“动词”与参数不匹配,例如 fmt.Printf("%d", "three")

    Gopls 提供了数十种来自各种套件的分析器;完整的列表请参阅 分析器。由分析器生成的每个诊断信息的 source 字段会记录生成它的分析器的名称。

    A diagnostic due to an analysis finding

    上面的示例显示了一个 printf 格式化错误。该诊断信息包含指向 printf 分析器文档的链接。

还有一个可选的第三类诊断信息来源

  • 编译器优化详细信息 是报告与 Go 编译器优化决策相关的详细信息的诊断信息,例如变量是否会逃逸或切片索引是否需要边界检查。

    优化决策包括:变量是否逃逸,以及如何推断逃逸;是否隐含了 nil 指针检查或消除了 nil 指针检查;以及函数是否可以内联。

    此来源默认禁用,但可以通过调用 source.toggleCompilerOptDetails (“{显示,隐藏} 编译器优化详细信息”) 代码操作按包启用。

    请记住,编译器优化器仅在没有编译错误的包上运行,因此优化诊断信息不会显示在无法构建的包上。

诊断信息的重新计算

默认情况下,每次修改源文件时都会自动重新计算诊断信息。

打开文件中的编译错误会在每次文件更改后(可能在每次按键后)很短的延迟(几十毫秒)后更新。这确保了在编辑时能快速反馈语法和类型错误。

整个工作区的编译和分析诊断信息的计算成本很高,因此它们通常在编辑后短暂空闲一段时间(约 1 秒)后重新计算。

diagnosticsDelay 设置决定了这个时间段。或者,可以使用 diagnosticsTrigger 设置,仅在保存已编辑的文件后触发诊断信息。

当使用 "pullDiagnostics": true 初始化时,gopls 还支持 “拉取诊断”,这是一种替代的重新计算诊断信息机制,在这种机制中,客户端使用 textDocument/diagnostic 请求显式地从 gopls 请求诊断信息。此功能默认关闭,直到拉取诊断的性能与推送诊断相当。

快速修复

每个分析器诊断信息都可以通过编辑代码提供一种或多种替代的修复方法。例如,当 return 语句的返回值数量不足时,fillreturns 分析器会建议一种修复方法,该方法会启发式地用合适的值填充缺失的返回值。应用修复即可消除编译错误。

An analyzer diagnostic with two alternative fixes

上面的截图显示了 VS Code 对“未使用参数”分析诊断信息的快速修复菜单,其中包含两个替代修复选项。(有关详细信息,请参阅 删除未使用参数。)

明确安全的建议修复是 "source.fixAll" 类型的 代码操作。许多客户端编辑器都有一个快捷键来应用所有此类修复。

TODO(adonovan):审核所有分析器,确保其文档与它们建议的任何修复措施保持同步。

设置

  • diagnosticsDelay 设置决定了编辑后诊断信息重新计算前的空闲时间。
  • diagnosticsTriggerr 设置决定了哪些事件会触发诊断信息的重新计算。
  • linkTarget 设置指定了 Diagnostic.CodeDescription 字段中 Go 包链接的基础 URI。

客户端支持

  • VS Code:每个诊断信息会显示为波浪线。悬停鼠标会显示详细信息以及任何建议的修复。
  • Emacs + eglot:每个诊断信息会显示为波浪线。悬停鼠标会显示详细信息。使用 M-x eglot-code-action-quickfix 应用可用的修复;如果有多个修复,它会提示您选择。
  • Vim + coc.nvim: ??
  • CLIgopls check file.go

stubMissingInterfaceMethods:声明接口缺失的方法

当具体类型的值被赋给接口类型变量,但具体类型缺少所有必需的方法时,类型检查器将报告“缺失方法”错误。

在这种情况下,gopls 提供了一个快速修复,用于向具体类型添加所有缺失方法的存根声明,使其实现接口。

例如,此函数将无法编译,因为值 NegativeErr{} 没有实现“error”接口

func sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, NegativeErr{} // error: missing method
    }
    ...
}

type NegativeErr struct{}

Gopls 将提供一个快速修复来声明此方法


// Error implements error.Error.
func (NegativeErr) Error() string {
    panic("unimplemented")
}

请注意,新的声明会出现在具体类型旁边,这可能与光标位置在不同的文件甚至不同的包。(也许 gopls 应该发送一个 showDocument 请求导航客户端到那里,或者发送一个进度通知来指示发生了什么。)

StubMissingCalledFunction:声明缺失的方法 T.f

当您尝试在没有该方法的类型上调用方法时,编译器将报告类似“type X has no field or method Y”的错误。在这种情况下,gopls 现在提供一个快速修复,用于生成缺失方法的存根声明,并根据调用推断其类型。

考虑以下代码,其中 Foo 没有 bar 方法

type Foo struct{}

func main() {
  var s string
  f := Foo{}
  s = f.bar("str", 42) // error: f.bar undefined (type Foo has no field or method bar)
}

Gopls 将提供一个快速修复,“声明缺失的方法 Foo.bar”。调用时,它会创建以下声明

func (f Foo) bar(s string, i int) string {
    panic("unimplemented")
}

CreateUndeclared:创建“未声明名称:X”的缺失声明

Go 编译器错误“未声明名称:X”表示变量或函数在使用前未在当前作用域中声明。在这种情况下,gopls 提供了一个快速修复来创建声明。

声明新变量

当您引用尚未声明的变量时

func main() {
  x := 42
  min(x, y) // error: undefined: y
}

快速修复将根据上下文推断其类型并插入一个默认值的声明

func main() {
  x := 42
  y := 0
  min(x, y)
}

声明新函数

同样,如果您调用了一个尚未声明的函数

func main() {
  var s string
  s = doSomething(42) // error: undefined: doSomething
}

Gopls 将在下方插入一个新的函数声明,根据调用推断其类型

func main() {
  var s string
  s = doSomething(42)
}

func doSomething(i int) string {
  panic("unimplemented")
}

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