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.org.tw/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.org.tw/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.org.tw/issue/41870),並且重新命名通常不能用於修復型別錯誤(https://golang.org.tw/issue/41851)。在重構時,我們建議分小步進行,並在進行過程中修復任何問題,以便在每一步儘可能多地編譯程式。
  • 有時,重新命名操作改變程式的引用結構是有益的,例如透過將 y 重新命名為 x 來有意組合兩個變數 xy。重新命名工具過於嚴格,無法在此情況下提供幫助(https://golang.org.tw/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.org.tw/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.org.tw/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 下找到。