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.org.tw/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.org.tw/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 下找到。