A Day In The Life

とあるプログラマの備忘録

go-mode + eglotを使ってEmacsのGolang開発環境を整える

普段 Golang でサーバコードを書くときはもっぱら Emacs を使っています。Go Modules に移行してから gocode が動作しなくなったので最近はやりの LSP(Language Server Protocol) を導入することにしました。Go の Language Server は gopls 、Emacs のLSPクライアントは eglot を使います

動作確認バージョン

Emacsは25以上、Golangは1.11以上なら問題なく動作するはずです。

gopls のインストール

Go の Language Server は3種類あります。一番下の gopls が Golang の公式 Language Server です。

一長一短あるようですが公式が安心そうなので gopls をインストールします。

$ go get -u golang.org/x/tools/cmd/gopls 

goimportsのインストール

Go の import を整理してくれるコマンドもついでにインストールしておきます。

$ go get golang.org/x/tools/cmd/goimports

init.el の設定

Emacs の LSP クライアントは lsp-mode が有名ですが、動作がめちゃくちゃ重かったのと余計な表示がいっぱい出てきたのでシンプルな eglot を使うことにしました。入力補完は company-mode を使います。あと GOPATH や GOROOT など環境変数設定のために exec-path-from-shell を使います。

;; パッケージ管理サーバ
(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
(package-initialize)
;; パッケージ情報の更新
(package-refresh-contents)

;; インストールするパッケージのリスト
(defvar my/favorite-packages
  '(
    use-package
    exec-path-from-shell
    ))
;; my/favorite-packagesからインストールしていないパッケージをインストール
(dolist (package my/favorite-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; シェルに設定されている環境変数を引き継ぐ
(exec-path-from-shell-initialize)

;; eglot
;; M-.で定義ジャンプ、M-,でジャンプ先からもどる
;; eglot はデフォルトの Language Server として go-langserver を使うので golsp に変更する
;; 事前に go get -u golang.org/x/tools/cmd/gopls しておくこと
(use-package eglot
  :ensure t ;自動インストール
  :config
  (define-key eglot-mode-map (kbd "M-.") 'xref-find-definitions)
  (define-key eglot-mode-map (kbd "M-,") 'pop-tag-mark)
  (add-to-list 'eglot-server-programs '(go-mode . ("gopls")))
  (add-hook 'go-mode-hook #'eglot-ensure))

;; company-mode
(use-package company
  :ensure t
  :config
  (global-company-mode)
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq company-dabbrev-downcase nil)
  (setq company-selection-wrap-around t))

(use-package go-mode
  :ensure t
  :commands go-mode
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook 'gofmt-before-save))

emacs-lispのコードはこちらに置いてます。

プロジェクトルートが見つからない時の対策

Monorepo 構成で Go プロジェクトが複数存在する場合に eglot がプロジェクトルートが判別できずローカルのパッケージの補完ができないことがあります(eglotはproject.elをつかってgitのファイル構成からルートを判別するようです)。その場合は projectile-mode を使ってプロジェクトルートを判別させます。init.elに以下のスクリプトを追加してから

;; Bridge projectile and project together so packages that depend on project
;; like eglot work
(use-package projectile
  :ensure t)
(defun my-projectile-project-find-function (dir)
  (let ((root (projectile-project-root dir)))
    (and root (cons 'transient root))))
(projectile-mode t)
(with-eval-after-load 'project
  (add-to-list 'project-find-functions 'my-projectile-project-find-function))

こんな感じでプロジェクトルートのディレクトリに空の . projectile ファイルを配置してください。

├ .git
├ foo_service
    ├ main.go
    └ .projectile
├ bar_service
    ├ main.go
    └ .projectile
└ baz_service
    ├ main.go
    └ .projectile

保管で Keyword argument のエラーが出た時の対処方法

error in process filter: Keyword argument :version not one of (:uri :diagnostics) のエラーが出るときは eglot のバージョンアップをすると解消します。

参考