💡

2023年版VimでGoを書く

2023/06/05に公開
2

あんまり使い込めてない構成にも言及してるので、浅い記事ですいません。

概要

大きく分けて、Go 専用プラグイン vim-go と、様々な言語に使える LSP クライアントの二種類があります。

どちらも基本は gopls に立脚しているので似たようなものなのですが、微妙に機能に差異があるので、実際に触って確かめるといいと思います。また、併用も可能ですがその場合ちょっと複雑になります。

LSP とは

最近のエディタを語るのに欠かせないのが、Language Server Protocol, 略して LSP です。知らなかった方は是非ここで覚えていってください。
https://microsoft.github.io/language-server-protocol/

ざっくり言うと、このプロトコルを通してエディタと「言語サーバー」が通信する仕組みのことです。このプロトコルさえ実装していれば、言語サーバーが一つあるだけで、あらゆるエディタに対して補完やエラー検出などの機能を提供できます[1]。(今までは各エディタごとに各言語拡張が必要だった)

で、Go における言語サーバーの名前が gopls です。公式にメンテされており、これ一択です。ごーぷりーずと読みます。https://github.com/golang/tools/tree/master/gopls

一方、エディタ側の LSP クライアントは、Vim に限定しても非常に様々な選択肢が存在します。ここでは解説しきれないため、詳細は他を参照してください。

  • vim-lsp (Pure Vim script でインストールが簡単)
  • Neovim builtin LSP (Neovim ならこっちのほうが簡単)
  • coc.nvim (超機能豊富な独自エコシステム)

vim-go について

https://github.com/fatih/vim-go

老舗の Go 専用 Vim プラグインです。

LSP は非常に便利なのですが、とはいえ gopls が提供していない機能だったり、LSP に乗せるのが厳しい言語固有の機能を埋めるには、言語専用プラグインがあった方が結局便利だったりします。その Vim 版が vim-go です。ちなみに VSCode も結局 Go 拡張機能が gopls には存在しないもろもろの機能を提供していたと思います。

固有の機能としては、「構造体のフィールドを自動で埋める」といった Go 固有の編集操作や、デバッガとの連携などが含まれます。これらを使いたいときは vim-go が第一候補に上がります。個人的にはカーソル下のテスト関数を実行する機能を使いたくて入れたり入れなかったりしています。

比較

じゃあ vim-go が最も優れているのかと言われると難しいところで、他の汎用的な LSP クライアントを使う利点には以下があります。

  • キーマップなどの設定を複数言語で使い回せる。
  • LSP を取り巻くより広範囲なエコシステムに乗っかることができる。

たとえば下のスクリーンショットは、筆者が vim-lsp で diagnostics を virtual text に表示する機能を利用している様子です。これはたぶん vim-go では実現不可能だったと思います。

筆者としては、まず汎用 LSP クライアント中心に構築することをおすすめします。その上で時折 vim-go のドキュメントを見て、魅力的な機能があれば乗り換えや併用を検討するとよいと思います。(vim-go なしでもなんとかはなるが、やはりあった方が便利な機能は vim-go には多い)

逆に、すでに vim-go に馴染んでいる方は無理に他の LSP クライアントに移行する必要はそこまでありません。気が向いたらでよいと思います。

細かい構築手順はこの記事では割愛する[2]ので、以下はスムーズに Vim で Go を書くための細々とした知見となります。

Tips

複数のリポジトリを同時に開くと壊れる

複数のリポジトリ、というか複数のモジュールを同時に開くには、事前に workspace の設定を行い、どのモジュールが同時に開かれるか明示することが必要です。詳しくはこちらを参照してください。https://github.com/golang/tools/blob/master/gopls/doc/workspace.md

他にも、依存モジュールをローカルにあるファイルに置き換える replace 機能なども workspace を使うとうまいこと制御できるので、是非覚えておいてください。

依存モジュールを追加したのに反映されない

go mod tidy なんかで go.mod に依存を追記してもすでに起動済みの gopls は それを反映してくれません。再起動が必要です。 gopls は workspace/didChangeWatchedFiles をサポートしているのでファイルを変更したらいい感じに反映してくれるはずですが、vim-lsp がこれをサポートしていなかったようです。 https://github.com/prabirshrestha/vim-lsp/pull/1048

vim-lsp なら :LspStopServer で gopls を停止した後、:e でファイルを開き直すことで反映されます。(でも、本当はコマンド一つで再起動したい...)

vim-go オンリーで gopls を再起動する方法は見当たりませんでした。どなたかご存知でしたら教えてください。

コメント欄にて教えていただきました。 :GoModReload コマンドで依存を再読み込みできます。また、単にリスタートしたい場合は :call go#lsp#Restart() とすることも可能です。(thanks @ryicoh!)

goimports について

自動で import 文を補完してくれるコマンド goimports ですが、全く同じものが source.organizeImports コードアクションとして gopls に搭載されているので、もはや不要です。

以下は vim-lsp で、保存時にフォーマット (LspDocumentFormatSync) した後に続けて import 文補完 (LspCodeActionSync source.organizeImports) を行う設定です。

autocmd! BufWritePre *.go call execute('LspDocumentFormatSync') | call execute('LspCodeActionSync source.organizeImports')

vim-go の保存時 import 補完について

vim-go は上記の保存時 import 補完の設定を自動で行ってくれます。なので併用すると2度補完が走ってムダに重くなってしまいます。併用時は vim-go にまかせて何も設定しないようにしましょう。

build tag について

build tag (//go:build ...) があるファイルでは gopls はうまく作動しません。LSP の詳しい設定で環境変数をセットすれば動くっちゃ動きますが、グローバルな起動時設定なのでホイホイ切り替えるわけにもいかず、まあまあ不便ですね。issue もあがっていますが、当面我慢するしかなさそうです。
https://github.com/golang/go/issues/29202

まとめ

Vim で Go を書く手段は様々ですがどれも gopls を利用しているのでだいたい同じです。しかし細かなところで差異はあるので、好みに合わせて選んでください。

脚注
  1. という建前 ↩︎

  2. 手抜きともいう ↩︎