Gopls:导航功能

本文档介绍了 gopls 用于导航源代码的功能。

定义

LSP 的 textDocument/definition 请求返回光标下符号声明的位置。大多数编辑器都提供了直接导航到该位置的命令。

定义查询在这些意外位置也有效

  • 导入路径上,它会返回导入包的文件中每个包声明的位置列表。
  • 包声明上,它会返回提供该包文档的包声明的位置。
  • go:linkname 指令中的符号上,它会返回该符号声明的位置。
  • 文档链接上,它会返回(类似于 hover)链接符号的位置。
  • go:embed 指令中的文件名上,它会返回嵌入文件的位置。
  • 在非 Go 函数(没有函数体的 func)的声明上,它会返回汇编实现的(如果有)的位置。
  • return 语句上,它会分别返回函数结果变量的位置。
  • gotobreakcontinue 语句上,它们会分别返回标签、相关块语句的结束花括号或相关循环的开始位置。

客户端支持 (Client support)

  • VS Code:使用 转到定义F12-click)。如果光标已位于声明处,则该请求将被解释为“转到引用”。
  • Emacs + eglot:使用 M-x xref-find-definitions
  • Vim + coc.nvim: ??
  • CLIgopls definition file.go:#offset

参考

LSP 的 textDocument/references 请求返回光标下符号所有引用的位置。

引用算法处理语法的各个部分如下

  • 符号的引用会报告该符号的所有使用。对于导出的符号,这可能包括其他包中的位置。
  • 包声明的引用是该包的所有直接导入,以及同一包中的所有其他包声明。
  • 内置符号(如 intappend)请求引用是错误的,因为它们被认为数量过多,不具有参考价值。
  • 接口方法的引用包括对实现该接口的具体类型的引用。类似地,对具体类型的方法的引用包括对相应接口方法的引用。
  • 结构体类型(如 struct{T})中的嵌入字段 T 在 Go 中是独一无二的,它既是类型(指向类型)的引用,又是字段(定义)的定义。references 操作仅报告其 作为字段的引用。要查找对类型的引用,请先跳转到类型声明。

请注意,引用查询仅返回用于分析所选文件的构建配置的信息,因此如果您请求一个在 foo_windows.go 中定义的符号的引用,结果将永远不包括文件 bar_linux.go,即使该文件引用了同名的符号;请参阅 https://golang.ac.cn/issue/65755

客户端可以请求将声明包含在引用中;大多数客户端都这样做。

客户端支持 (Client support)

  • VS Code:使用 转到引用 快速“预览”引用,或使用“查找所有引用”打开引用面板。
  • Emacs + eglot:通过 xref:使用 M-x xref-find-references
  • Vim + coc.nvim: ??
  • CLIgopls references file.go:#offset

实施

LSP 的 textDocument/implementation 请求查询抽象类型和具体类型及其方法之间的关系。

接口和具体类型使用方法集进行匹配

  • 当在对接口类型的引用上调用时,它会返回实现该接口的每种类型的声明的位置。
  • 当在对具体类型上调用时,它会返回匹配的接口类型的位置。
  • 当在对接口方法上调用时,它会返回满足该接口的类型的相应方法。
  • 当在对具体方法上调用时,它会返回匹配的接口方法的位置。

例如

  • implementation(io.Reader) 包括子接口,如 io.ReadCloser,以及具体实现,如 *os.File。它还包括等同于 io.Reader 的其他声明。
  • implementation(os.File) 只包括接口,如 io.Readerio.ReadCloser

LSP 的实现功能内置了对子类型的偏好,可能是因为在 Java 和 C++ 等语言中,类型与其超类型之间的关系在语法中是明确的,因此相应的“转到接口”操作可以通过一系列两次或多次“转到定义”步骤来实现:第一次访问类型声明,其余步骤依次访问祖先。(参见 https://github.com/microsoft/language-server-protocol/issues/2037。)

在 Go 中,类型之间没有语法关系,在子类型和超类型之间进行导航时都需要搜索。上述启发式方法在许多情况下效果很好,但无法查询 io.ReadCloser 的超接口。要更明确地在子类型和超类型之间进行导航,请使用 [类型层级结构](#Type Hierarchy) 功能。

仅考虑非平凡接口;对于类型 any,不报告任何实现。

在同一包内,会报告所有匹配的类型/方法。但是,跨包时,仅报告导出的包级类型及其方法,因此局部类型(无论是接口还是具有方法(由于嵌入)的结构体类型)可能在结果中缺失。

函数、func 类型和动态函数调用使用签名进行匹配

  • 当在函数定义func 关键字上调用时,它会返回匹配的签名类型和动态调用表达式的位置。
  • 当在签名类型func 关键字上调用时,它会返回匹配的具体函数定义的位置。
  • 当在动态函数调用( 符号上调用时,它会返回匹配的具体函数定义的位置。

如果目标类型或候选类型是泛型的,结果将包括候选类型,只要存在这两个类型中允许一个实现另一个的任何实例化。(注意:匹配器目前不实现完整的统一,因此类型参数被视为通配符,可以匹配任意类型,而无需考虑方法集内或单个方法内的替换一致性。这可能导致偶尔出现误匹配。)

由于一个类型可能既是函数类型又是具有方法的命名类型(例如,http.HandlerFunc),它可能参与两种类型的实现查询(按方法集和函数签名)。使用方法集的查询应在类型或方法名称上调用,而使用签名的查询应在 func( 符号上调用。

客户端支持 (Client support)

  • VS Code:使用 转到实现⌘F12)。
  • Emacs + eglot:使用 M-x eglot-find-implementation
  • Vim + coc.nvim: ??
  • CLIgopls implementation file.go:#offset

类型定义

LSP 的 textDocument/typeDefinition 请求返回所选符号类型的定义位置。

例如,如果选择的是局部变量 buf 的名称,其类型为 *bytes.Buffer,则 typeDefinition 查询将返回 bytes.Buffer 类型的定义位置。客户端通常会导航到该位置。

指针、数组、切片、通道和映射等类型构造函数在查找命名类型时会被从所选类型中剥离。例如,如果 x 的类型是 chan []*T,则报告的类型定义将是 T 的定义。类似地,如果符号的类型是一个具有一个“有趣”(命名、非 error)结果类型的函数,则将使用该函数的返回类型。

Gopls 目前要求 typeDefinition 查询应用于符号,而不是任意表达式;有关此功能可能扩展的信息,请参见 https://golang.ac.cn/issue/67890

客户端支持 (Client support)

  • VS Code:使用 转到类型定义
  • Emacs + eglot:使用 M-x eglot-find-typeDefinition
  • Vim + coc.nvim: ??
  • CLI:不支持

文档符号

textDocument/documentSymbol LSP 查询报告此文件中顶级声明的列表。客户端可以使用此信息来展示文件概览,以及用于更快导航的索引。

如果客户端指示了 hierarchicalDocumentSymbolSupport,Gopls 将响应 DocumentSymbol 类型;否则,它将返回 SymbolInformation

客户端支持 (Client support)

  • VS Code:使用 大纲视图 进行导航。
  • Emacs + eglot:使用 M-x imenu 跳转到符号。
  • Vim + coc.nvim: ??
  • CLIgopls links file.go

符号

workspace/symbol LSP 查询会搜索工作区中所有符号的索引。

默认符号匹配算法(fastFuzzy),灵感来自流行的模糊匹配器 FZF,它会尝试各种不精确的匹配来纠正查询中的拼写错误或缩写。例如,它将 DocSym 视为 DocumentSymbol 的匹配项。

设置 (Settings)

客户端支持 (Client support)

  • VS Code:使用 ⌘T 打开具有工作区范围的 转到符号。(或者,使用 Ctrl-Shift-O,并在前面加上 @ 前缀在文件内搜索,或加上 # 前缀在整个工作区中搜索。)
  • Emacs + eglot:使用 M-x xref-find-apropos 来显示与搜索词匹配的符号。
  • Vim + coc.nvim: ??
  • CLIgopls links file.go

选择范围

textDocument/selectionRange LSP 查询会返回有关当前选择所包含的每个语法片段的词法范围的信息。客户端可以使用它来提供一个将选择扩展到更大表达式的操作。

客户端支持 (Client support)

  • VSCode:使用 ⌘⇧^→ 扩展选择,或使用 ⌘⇧^← 再次收缩选择;观看此 视频
  • Emacs + eglot:非标准。使用 此配置片段 中定义的 M-x eglot-expand-selection
  • Vim + coc.nvim: ??
  • CLI:不支持

调用层级结构

LSP CallHierarchy 机制包含三个查询,它们共同使客户端能够呈现静态调用图一部分的层级视图。

在函数声明中选择名称时调用此命令。

动态调用不包含在内,因为在分析上检测它们并不实际。因此,请注意结果可能不详尽,并在必要时执行 引用 查询。

该层级结构不将嵌套函数视为与其包含的命名函数不同。(由于无法检测动态调用,因此这样做意义不大。)

下面的屏幕截图显示了以 f 为根的传出调用树。该树已展开,显示了从 ffmt.Sprint 内部的 fmt.StringerString 方法的路径:

客户端支持 (Client support)

  • VS Code显示调用层级结构 菜单项(⌥⇧H)打开 调用层级结构视图(注意:文档引用的是 C++,但对 Go 的概念是相同的)。
  • Emacs + eglot:非标准;使用 (package-vc-install "https://github.com/dolmens/eglot-hierarchy") 安装。使用 M-x eglot-hierarchy-call-hierarchy 显示所选函数的直接传入调用;使用前缀参数(C-u)显示直接传出调用。无法展开树。
  • CLIgopls call_hierarchy file.go:#offset 显示传出和传入调用。

类型层级结构

LSP TypeHierarchy 机制包含三个查询,它们共同使客户端能够呈现命名类型上子类型关系一部分的层级视图。

在选择类型名称时调用此命令。

与实现查询一样,类型层级结构查询仅在查询类型的同一包内报告函数局部类型。此外,结果不包括别名类型,只包括定义类型。

注意事项 (Caveats)

  • 类型层级结构仅支持命名类型及其可赋值性关系。相比之下,实现请求还报告了无名 func 类型与函数声明、函数字面量和这些类型的值的动态调用之间的关系。

客户端支持 (Client support)

  • VS Code显示类型层级结构 菜单项打开 类型层级结构视图(注意:文档引用的是 Java,但对 Go 的概念是相同的)。
  • Emacs + eglot:于 2025 年 3 月添加支持。使用 M-x eglot-show-call-hierarchy
  • CLI:尚未支持。

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