Gopls:設計

來自未來的註記

以下是 gopls 的原始設計文件,彙集自 2018 年至 2019 年的各種來源。自那以後,下面列出的所有功能以及許多其他功能都已實現。前兩個目標已實現:gopls 是 LSP 的完整實現,也是 VS Code Go 和許多其他編輯器預設的後端。第三個目標僅部分實現:雖然 gopls 已經獲得了許多功能,但它並不像本文件中所用的那樣具有可擴充套件性:擴充套件 gopls 的唯一方法是修改 gopls。第四個目標尚未實現:儘管一些知名公司能夠使用 gopls 和 Bazel,但體驗不盡人意,Go 命令是唯一官方支援的構建系統。

另一方面,兩個明確的非目標已被重新考慮。其中一個很小:透過語義令牌,LSP 現在支援語法高亮。另一個很重要:隨著 gopls 的普及,人們發現其記憶體佔用是一個問題。開發工作區的規模增長速度超過了典型開發環境中(尤其是在容器化開發中)可用的 RAM。Gopls 現在使用磁碟索引和記憶體快取的混合體,更詳細的資訊請參閱我們關於可擴充套件性的 部落格文章

值得注意的是,本文件在預測困難方面出奇地準確。Gopls 確實在它所構建的核心標準庫包方面遇到了困難,其使用者體驗仍然受限於 LSP。儘管如此,堅持使用標準庫和 LSP 是正確的做法,因為儘管我們團隊規模很小,但這些決定幫助 gopls 跟上了不斷發展的 Go 語言(例如泛型),並與許多新的文字編輯器整合。

四年多後,gopls 的開發仍在繼續,重點是簡潔性、可靠性和可擴充套件性。新的、選擇加入的 Go 遙測 將幫助我們達到比僅透過 Github 問題所能達到的更高的版本穩定性標準。此外,遙測將使我們能夠專注於高優先順序功能,並棄用那些給程式碼庫帶來負擔的歷史變通方法。隨著速度的提高,我們期待與社群合作,改進重構、靜態分析以及未來可能出現的任何東西。

  • Rob Findley (rfindley@google.com),2023 年

目標

  • gopls 應該成為 Go 程式設計師使用的主要編輯器後端,並由 Go 團隊提供全面支援。
  • gopls 將是 LSP 的完整實現,如 LSP 規範中所述,以儘可能標準化其大部分功能。
  • gopls 將是簡潔且可擴充套件的,以便將來能夠包含更多功能,使 Go 工具再次成為同類最佳。
  • gopls支援備用的構建系統和檔案佈局,從而在任何環境中使 Go 開發更簡單、更強大。

Context

雖然 Go 有許多出色且有用的命令列工具可以增強開發人員的體驗,但將這些工具與 IDE 整合可能具有挑戰性,這一點已經很清楚。

對這些工具的支援一直依賴於社群成員的善意,並且隨著語言、工具鏈和環境的變化,他們有時會承擔巨大的支援負擔。結果是許多工具停止工作,出現支援問題,或因分叉和替代品而變得混亂,或提供的體驗不如應有的好。有關更多問題和詳細資訊,請參閱下面的 現有解決方案部分。

對於偶爾使用的工具來說,這沒關係,但對於核心 IDE 功能來說,這是不可接受的。自動完成、跳轉到定義、格式化和其他此類功能應始終有效,因為它們是 Go 開發的關鍵。

Go 團隊將建立一個適用於任何構建系統的編輯器後端。它還將能夠提高 Go 工具的延遲,因為每個工具將不再需要在每次呼叫時單獨執行型別檢查器,而是會有一個長期執行的程序,並且資料可以在定義、完成、診斷和其他功能之間共享。

透過承擔這些工具的所有權並將它們打包成 gopls 的形式,Go 團隊將確保 Go 開發人員的 Go 開發體驗不會不必要地複雜化。擁有一個編輯器後端將簡化 Go 開發人員、Go 團隊以及 Go 編輯器外掛維護者的生活。

有關更多背景資訊,請參閱 Rebecca 在 GopherCon 上的精彩主題演講 影片幻燈片

非目標

  • 命令列速度

    雖然 gopls 將具有命令列模式,但它將針對長期執行進行最佳化,而不是命令響應速度,因此它可能不適合 CI 系統等用途。對於這種情況,將必須有一個使用相同底層庫以確保一致性的備用工具。

  • 低記憶體環境

    為了能夠以極低的延遲出色地處理大型專案,gopls 將在記憶體中保留大量資訊。人們認為開發人員通常在具有大量 RAM 的系統上工作,這不會成為問題。總的來說,這得到了現有 IDE 解決方案(如 IntelliJ)的大記憶體使用量的支援。

  • 語法高亮

    目前沒有編輯器將此功能委託給單獨的二進位制檔案,也沒有標準化的方法來執行此操作。

現有解決方案

每年,Go 團隊都會進行一項調查,詢問開發人員關於他們使用該語言的經驗。

其中一個問題是“你對你的編輯器感覺如何?”

回覆講述了一個非常負面的故事。一些分類引述

  • 設定
    • “安裝和配置困難”
    • “文件不足”
  • 效能
    • “效能非常差”
    • “在大專案中相當慢”
  • 可靠性
    • “功能有一天有效,第二天就失效了”
    • “工具鏈沒有隨著新的語言功能而更新”

每個編輯器都有自己的外掛,該外掛會呼叫各種工具,其中許多工具會因新的 Go 版本而中斷,或者因為它們不再維護。隨著語言、工具鏈和環境的變化,它們有時會承擔巨大的支援負擔。

每個工具都必須自己完成理解程式碼及其所有傳遞性依賴的工作。

每個功能都是一個不同的工具,具有不同的命令列模式,接受輸入和解析輸出的不同方式,指定原始碼位置的不同方式。為了支援其現有功能集,VSCode 安裝了 24 個不同的命令列工具,其中許多工具具有用於配置的選項或分叉。在檢視需要遷移到模組的工具集時,跨所有編輯器共有 63 個單獨的工具。

所有這些工具都需要理解程式碼,並且它們使用相同的標準庫來完成這項工作。這些庫針對此類工具進行了最佳化,但即便如此,處理大量程式碼仍然需要大量時間。幾乎沒有工具能夠在 100 毫秒內返回結果。當開發人員在編輯器中輸入時,需要啟用多個此類功能,這意味著他們不僅要支付一次費用,還要多次支付。整體效果是編輯體驗感覺遲鈍,功能要麼未啟用,要麼有時產生的結果出現得如此之慢,以至於在到達時已不再有用。這個問題隨著程式碼庫的增大而增加,這意味著它會隨著時間的推移而變得更糟,並且對於公司在更重要的任務中使用 Go 時處理的大型程式碼庫尤其糟糕。

要求

完整的功能集

要使 gopls 獲得成功,它必須實現 下文中討論的全部功能集。這是使用者在能夠像使用它所取代的工具一樣高效地工作所需的功能集。它不包括以前實現中的所有功能,有些功能幾乎從未使用過,應該被刪除(例如 guru 的指標分析),還有一些功能不容易契合,需要變通(替換儲存鉤子/linter)。

同等或更好的體驗

對於所有這些功能,使用者體驗必須與所有編輯器中提供的當前體驗相匹配或超出。這是一個容易提出的宣告,但很難驗證或衡量。許多可能的測量方法都未能捕捉到這種體驗。

例如,如果嘗試測量跳轉到定義呼叫的延遲,與舊的 godef 工具相比,結果將相當一致。從 gopls 實現來看,延遲範圍可能大得多,最好的可能快幾個數量級,最差的則略差,因為 gopls 嘗試做更多的工作,但設法在呼叫之間快取它。

或者對於一個補全呼叫,它可能更慢,但能產生更好的第一個匹配項,從而使使用者更頻繁地接受它,從而獲得更好的整體體驗。

在大多數情況下,這必須依賴使用者報告。如果使用者因為體驗沒有更好而拒絕切換,那顯然還沒有完成;如果他們切換了,但大多數人都在抱怨,那可能有很多方面得到了改進,足以促使切換,但其他方面卻有所欠缺。如果大多數人都在切換,並且保持沉默或表示積極,那可能就完成了。在編寫工具時,使用者就是一切。

穩定的貢獻者社群

gopls 試圖解決的問題的範圍和規模對於核心 Go 團隊來說是不可承受的,這需要一個強大的社群才能實現這一切。

這意味著程式碼必須易於貢獻,並且許多開發人員可以輕鬆地並行工作。功能需要良好地解耦,並有完善的測試故事。

延遲在使用者可容忍範圍內

關於使用者可接受操作延遲的研究已經有很多。

對 gopls 影響最大的結果是,直接響應連續使用者操作的反饋需要在 100 毫秒以內才能被感知不到,而超過 200 毫秒會惹惱使用者。這意味著總的來說,任何在開發人員輸入時發生的事情的目標都必須是 <100 毫秒。總會有 gopls 無法滿足此截止日期的情況,並且需要有方法在這些情況下使使用者體驗良好,但總的來說,此截止日期的目的是為基本架構設計提供資訊,任何理論上無法長期滿足此目標的解決方案都是錯誤的。

易於配置

開發人員非常挑剔,並且在他們的編碼體驗方面有著截然不同的願望。gopls 將不得不支援相當大的靈活性,以滿足這些願望。然而,預設設定在沒有任何配置的情況下必須是大多數使用者體驗最佳的設定,並且在可能的情況下,功能必須是靈活的,無需配置,以便客戶端可以輕鬆地做出有關處理的決策,而無需更改與 gopls 的通訊。

困難

資料量

  • 企業單體倉庫:大得多

解析和型別檢查大量程式碼的成本非常高,並且轉換後的形式佔用大量空間。由於 gopls 在開發人員輸入時必須不斷更新這些資訊,因此它需要非常小心地管理其轉換形式的快取,以平衡記憶體使用與速度。

快取無效

型別檢查的基本操作單位是包,但編輯器的基本操作單位是檔案。gopls 需要能夠有效地將檔案對映到包,以便在檔案更改時知道哪些包需要更新(以及任何其他以傳遞方式依賴它們的包)。事實使這變得特別困難,即更改檔案的內容會修改它被視為一部分的包(透過更改包宣告或構建標籤),一個檔案可以屬於多個包,並且可以在不使用編輯器的情況下對檔案進行更改,在這種情況下,它不會通知我們更改。

不合適的內建功能

Go 的基礎庫(如 go/tokengo/astgo/types)都是為類似編譯器的應用程式設計的。它們更關注吞吐量而不是記憶體使用,它們具有旨在在程式退出時增長然後被丟棄的結構,並且它們不是為在原始檔存在錯誤的情況下持續執行而設計的。它們也無法進行增量更改。

讓長期執行的服務與這些庫良好協作是一個巨大的挑戰,但編寫新庫將花費更多工作,並帶來重大的長期成本,因為兩套庫都必須維護。目前,將工作工具交到使用者手中更為重要。從長遠來看,這一決定可能需要重新考慮,新的底層庫可能是推動能力向前發展的唯一途徑。

構建系統功能

gopls 被認為是與構建系統無關的,但它必須使用構建系統來發現檔案如何對映到包。當它嘗試這樣做時,即使功能相同,成本(時間、CPU 和記憶體)也大不相同,並可能嚴重影響使用者體驗。設計 gopls 如何與構建系統互動以嘗試最小化或隱藏這些差異是很困難的。

構建標籤

Go 的構建標籤系統非常強大,並且有許多用例。原始檔可以使用關於活動標籤集的強大布爾邏輯來排除自身。然而,它旨在指定命令列上的活動標籤集,並且所有庫都旨在一次處理一個有效的組合。也沒有辦法找出有效組合的集合。

型別檢查一個檔案需要了解同一包中的所有其他檔案,並且該檔案集會受到構建標籤的影響。包的匯出識別符號集也受到包中檔案數量的影響,因此也受其構建標籤的影響。

這意味著即使對於沒有構建標籤控制的檔案或包,也無法在不知道要考慮的構建標籤集的情況下產生正確的結果。這使得在檢視檔案時很難產生有用的結果。

LSP 不支援的功能

有些事情可以做得很好,但與現有 LSP 協議不太契合。例如,顯示控制流資訊、自動結構標籤、複雜重構……

每個功能都必須仔細考慮,並提出對 LSP 的更改,或新增一種方式來擁有 gopls 特定的協議擴充套件,這些擴充套件仍然易於在所有編輯器外掛中使用。

為了避免這些,一開始只實現核心 LSP 功能,因為它們足以滿足基本要求,但潛在的功能需要在核心架構中牢記。

分發

確保使用者使用正確版本的 gopls 將是一個問題。每個編輯器外掛可能會以自己的方式安裝工具,有些會選擇全域性安裝,有些會保留自己的副本。

由於它是一個全新的工具,它將快速變化。如果使用者沒有意識到他們使用的是舊版本,他們將遇到已經修復的問題,這對他們來說更糟,然後可能會報告這些問題,這會浪費 gopls 團隊的時間。需要有一個機制來檢查 gopls 是否是最新版本,以及一種推薦的安裝最新版本的方法。

除錯使用者問題

gopls 本質上是開發人員機器上一個非常狀態化且長期執行的伺服器。它的基本操作受到許多因素的影響,從使用者的環境到本地構建快取的內容。它正在處理的資料通常是保密的 कोड庫,無法共享。所有這些因素都使得使用者難以有效地報告錯誤或建立最小的重現。

需要有簡便的方法供使用者報告他們可以提供的資訊,並有方法在不獲取其全部狀態的情況下重現問題。這對於生成迴歸測試也是必需的。

基本設計決策

有一些根本性的架構決策會影響該工具的其餘大部分設計,做出影響使用者體驗的基本權衡。

程序生命週期:由編輯器管理

處理大型程式碼庫以進行完整型別檢查和分析(在延遲要求內)是不可行的,也是現有解決方案的主要問題之一。即使計算出的資訊被快取到磁碟,這仍然成立,因為執行分析器和型別檢查器最終需要依賴圖中所有檔案的完整 AST。理論上可以做得更好,但只能透過重寫現有的解析和型別檢查庫來完成,這在目前是不可行的。

這意味著 gopls 應該是一個長期執行的程序,它能夠快取和預先計算記憶體中的結果,以便在收到請求時能夠更快地提供答案。

它可以作為使用者機器上的守護程序執行,但管理守護程序有很多問題。從長遠來看,這可能是正確的選擇,並且應該在基本架構設計中允許它,但一開始它將有一個程序,該程序的持續時間與啟動它的編輯器一樣長,並且可以輕鬆重啟。

快取:記憶體中

持久的磁碟快取維護成本很高,並且需要解決許多額外的問題。儘管構建所需資訊與請求所需的延遲相比成本很高,但與編輯器的啟動時間相比,它相對較小,因此預計在 gopls 重啟時重建資訊是可以接受的。

這樣做的好處是,gopls 在重啟之間是無狀態的,這意味著如果它出現問題或狀態混亂,簡單的重啟通常可以解決問題。它還意味著當用戶報告問題時,不需要整個磁碟快取狀態來診斷和重現問題。

通訊:stdin/stdout JSON

LSP 規範定義了通常使用的 JSON 訊息,但它沒有定義這些訊息應如何傳送,並且存在不使用 JSON 的 LSP 實現(例如,Protocol buffers 是一個選項)。

gopls 的限制是它必須易於整合到所有編輯器所有作業系統上,並且不應有大的外部依賴。

JSON 是 Go 標準庫的一部分,也是 LSP 的原生語言,因此最符合邏輯。到目前為止,最受支援的通訊機制是程序的標準輸入和輸出,並且通用客戶端實現都有使用 JSON rpc 2 的方法。Go 中沒有此協議的完整且依賴項很少的實現,但它是一個相當小的協議,基於 JSON 庫,可以透過中等努力來實現,並且無論如何都將是一個普遍有用的庫。

將來預計它將以分離的客戶端伺服器模式執行,因此從一開始就以可以使用套接字而不是 stdin/stdout 的方式編寫它,是確保它仍然可行的方法。能夠手動執行 gopls 伺服器並在編輯器外部進行監視/除錯也是一個巨大的除錯輔助。

執行其他工具:

功能

gopls 需要公開一組功能才能成為全面的 IDE 解決方案。以下是功能集,以及它們現有的解決方案以及應如何對映到 LSP。

內省

內省功能在開發人員工作時提供有關其程式碼的資訊。它們不進行或建議更改。


Diagnostics 程式碼的靜態分析結果,包括編譯和 lint 錯誤
需要 完整的 go/analysis 執行,需要完整的 AST、型別和 SSA 資訊
LSP textDocument/publishDiagnostics
以前 go buildgo vetgolinterrcheckstaticcheck
這是最重要的 IDE 功能之一,它允許快速迭代,而無需在 shell 中執行編譯器和檢查器。通常用於為 IDE 中的問題列表、縮排標記和波浪線下劃線提供支援。
在讓使用者自定義要執行的檢查集方面,有一些複雜的設計工作要做,最好是無需重新編譯主 LSP 二進位制檔案。

懸停 (Hover) 有關游標下程式碼的資訊。
需要 檔案和所有依賴項的 AST 和型別資訊
LSP textDocument/hover
以前 godocgogetdoc
在閱讀程式碼時用於顯示編譯器已知但程式碼中並不總是明顯的資訊。例如,它可以返回識別符號的型別或文件。

簽名幫助 函式引數資訊和文件
需要 檔案和所有依賴項的 AST 和型別資訊
LSP textDocument/signatureHelp
以前 gogetdoc
在程式碼中鍵入函式呼叫時,瞭解該呼叫的引數以使開發人員能夠正確呼叫它很有幫助。

導航功能旨在讓開發人員更容易地在程式碼庫中找到方向。


定義 選擇一個識別符號,然後跳轉到該識別符號被定義的程式碼。
需要 檔案和所有依賴項的完整型別資訊
LSP textDocument/declaration
textDocument/definition
textDocument/typeDefinition
以前 godef
要求編輯器開啟符號定義位置是 IDE 中最常用的程式碼導航工具之一。在探索不熟悉的程式碼庫時尤其有用。
由於編譯器輸出的限制,無法為此任務使用二進位制資料(特別是它不知道列資訊),因此必須從原始檔解析。

實施 報告實現介面的型別
需要 完整的程式碼庫型別知識
LSP textDocument/implementation
以前 impl
此功能很難擴充套件到大型程式碼庫,需要仔細考慮才能正確實現。可能暫時可以實現一個更有限的形式。

文件符號 提供當前檔案中頂級符號的集合。
需要 僅當前檔案的 AST
LSP textDocument/documentSymbol
以前 go-outlinego-symbols
用於驅動大綱模式等功能。

參考 查詢游標下符號的所有引用。
需要 AST 和型別資訊(反向傳遞閉包)
LSP textDocument/references
以前 guru
這需要了解可能依賴於當前檔案所在包的每個包。過去,這要麼透過全域性知識實現(這無法擴充套件),要麼透過指定一個“範圍”來混淆使用者,以至於他們根本不使用這些工具。gopls 長期來看可能需要一個更強大的解決方案,但起初自動限制範圍可能會產生可接受的結果。這可能是模組(如果已知),否則是某個合理的父目錄。

摺疊 報告塊的邏輯層次結構
需要 僅當前檔案的 AST
LSP textDocument/foldingRange
以前 go-outline
這通常用於在編輯器中提供展開和摺疊行為。

選擇 報告游標周圍的邏輯選擇區域
需要 僅當前檔案的 AST
LSP textDocument/selectionRange
以前 guru
在編輯器功能(如展開選擇)中使用。

編輯助手

這些功能為使用者建議或應用程式碼編輯,包括重構功能,有許多潛在用例。重構是 Go 工具可能非常強大的領域之一,但迄今為止尚未實現,因此在開發人員體驗方面有巨大的改進潛力。然而,目前還沒有清楚地瞭解人們需要的重構型別以及它們應該如何表達,並且 LSP 協議在這方面存在弱點。這意味著它可能更像是一個研究專案。


格式化 修復檔案的格式
需要 當前檔案的 AST
LSP textDocument/formatting
textDocument/rangeFormatting
textDocument/onTypeFormatting
以前 gofmtgoimportsgoreturns
它將使用標準的格式包。
目前的限制是它不能處理格式錯誤的 कोड。可能需要對格式化程式進行一些非常仔細的更改,以允許格式化無效的 AST 或更改以強制 AST 進入有效模式。這些更改也將改進範圍和檔案模式,但對於 onTypeFormatting 來說基本上是必不可少的。

匯入 自動重寫匯入塊以匹配使用的符號。
需要 當前檔案的 AST 以及所有候選包的完整符號知識。
LSP textDocument/codeAction
以前 goimportsgoreturns
這需要了解尚未使用的包,以及按名稱查詢這些包的能力。
它還需要發現的所有包的匯出符號資訊。
它應該使用標準的匯入包來實現,但對於某些互動,可能需要暴露比僅重寫檔案更細粒度的 API。

自動完成 提出建議以完成當前正在鍵入的實體。
需要 檔案和所有依賴項的 AST 和型別資訊
還需要所有包的完整匯出符號知識。
LSP textDocument/completion
completionItem/resolve
以前 gocode
自動完成是最複雜的功能之一,它知道得越多,建議就越好。例如,如果它知道尚未匯入的包的公共符號,它可以自動完成。如果它知道你正在編寫的程式型別,它可以提出更好的選項建議。如果它知道你通常如何呼叫一個函式,它可以建議更好的引數。如果它知道它們是常見的程式碼模式,它可以建議整個程式碼模式。與許多有特定任務的功能不同,完成自動補全永遠不會完成。在候選者及其排名之間取得平衡和改進將是一個長期的研究問題。

重新命名 重新命名一個識別符號
需要 AST 和型別資訊(反向傳遞閉包)
LSP textDocument/rename
textDocument/prepareRename
以前 golang.org/x/tools/cmd/gorename
這使用了與查詢引用相同的​​資訊,具有相同的​​問題和限制。它稍微糟糕一些,因為它建議的更改使其對不正確的結果不容忍。使用它來更改包的公共 API 也很危險。

建議的修復 可以手動或自動接受以更改程式碼的建議
需要 完整的 go/analysis 執行,需要完整的 AST、型別和 SSA 資訊
LSP textDocument/codeAction
以前 不適用
這是一個由新的 go/analysis 引擎驅動的全新功能,它應該能夠實現大量的自動化重構。

本文件的原始碼可以在 golang.org/x/tools/gopls/doc 下找到。