A few months ago I went over how to set up Emacs for Go development. Since then, I have honestly not changed a single thing about it. Until this week.
Here’s the thing. Something I have changed too much over the last few months has been the vendoring mechanism I use for my Go projects. From Glide, I moved to go dep, and a couple of months ago, I started the migration to the Go Modules, Golang’s potential long-term solution to the package management mess the community has been living with since the inception of the language 10 years ago (Happy Birthday Go!).
Go Modules changes a lot of things about the taxonomy of your projects: vendor management, GOPATH,
go get, etc. Just like me and my vendor management journey,
gomode has also gone through several iterations from here, to here, and here.
It was time to start looking at something else with more long term support.
LSP (Language Server Provider) promises to do away with all these issues by implementing a “driver” with a common editor interface and adapting to the languages as they evolve. Rebecca Stambler’s “Go, please stop breaking my editor” is a must-watch if you’re interested in the topic.
To work on Go using emacs, the most logical recipe would be lsp-mode, gopls, and go-mode.
gopls is the LSP-compatible language server,
lsp-mode is the Emacs interface for LSP servers, and
go-mode is, well, the major mode for Go (discussed in my previous article).
In the video (May 2019), Rebecca points out that
gopls is still in alpha and warns us to proceed with caution. Currently, (December 2019) it feels more like a beta, so not too bad, but still not perfect.
gopls is, however, the future for Emacs to become a solid Golang editor. This is because it’s the only language tool (beside’s Rebecca’s
gocode that’s in maintenance mode) that fully supports Go modules that is in active development.
All I had to do to install it was this:
go get golang.org/x/tools/gopls@latest
It’s important to check out the latest version since the tool is evolving quite quickly right now.
What happens in the editor is that Emacs spawns an instance of the server when
go-mode is enabled on a buffer. After this,
lsp-mode will attempt to connect to it. The advantage of using a server rather than a command is that it’s much quicker to respond and interface with Emacs (it does feel snappier once the server comes up, which is also fairly quick). The disadvantages are the security concerns of running a server on a laptop; but to be fair, this is not supposed to be a long-running/permanent process.
On my Emacs config, I just added the following lines:
(setq lsp-gopls-staticcheck t) (setq lsp-eldoc-render-all t) (setq lsp-gopls-complete-unimported t)
I just wanted those elements to be active, but feel free to edit to taste.
lsp-mode will need to be added to our Emacs set up. I copied most of this from the
lsp-mode set up guides adjusting it to my needs and adding a couple of things.
(use-package lsp-mode :ensure t :commands (lsp lsp-deferred) :hook (go-mode . lsp-deferred)) ;;Set up before-save hooks to format buffer and add/delete imports. ;;Make sure you don't have other gofmt/goimports hooks enabled. (defun lsp-go-install-save-hooks () (add-hook 'before-save-hook #'lsp-format-buffer t t) (add-hook 'before-save-hook #'lsp-organize-imports t t)) (add-hook 'go-mode-hook #'lsp-go-install-save-hooks) ;;Optional - provides fancier overlays. (use-package lsp-ui :ensure t :commands lsp-ui-mode :init ) ;;Company mode is a standard completion package that works well with lsp-mode. ;;company-lsp integrates company mode completion with lsp-mode. ;;completion-at-point also works out of the box but doesn't support snippets. (use-package company :ensure t :config (setq company-idle-delay 0) (setq company-minimum-prefix-length 1)) (use-package company-lsp :ensure t :commands company-lsp) ;;Optional - provides snippet support. (use-package yasnippet :ensure t :commands yas-minor-mode :hook (go-mode . yas-minor-mode)) ;;lsp-ui-doc-enable is false because I don't like the popover that shows up on the right ;;I'll change it if I want it back (setq lsp-ui-doc-enable nil lsp-ui-peek-enable t lsp-ui-sideline-enable t lsp-ui-imenu-enable t lsp-ui-flycheck-enable t)
- Add hooks for package imports and buffer formatting on save.
- Company for overlays.
yasnippetfor snippet support.
- Some options for
In the options I disabled
lsp-ui-doc-enable because it displayed docs on both the mini buffer and on an overlay located to the right of the buffer. I found it too distracting and decided to disable the overlay and leave the mini buffer help.
Old Go Mode
So what’s left of my original Go mode configuration? Well, the custom compilation stuff, line numbers, etc are still there:
(defun custom-go-mode () (display-line-numbers-mode 1)) (use-package go-mode :defer t :ensure t :mode ("\\.go\\'" . go-mode) :init (setq compile-command "echo Building... && go build -v && echo Testing... && go test -v && echo Linter... && golint") (setq compilation-read-command nil) (add-hook 'go-mode-hook 'custom-go-mode) :bind (("M-," . compile) ("M-." . godef-jump))) (setq compilation-window-height 14) (defun my-compilation-hook () (when (not (get-buffer-window "*compilation*")) (save-selected-window (save-excursion (let* ((w (split-window-vertically)) (h (window-height w))) (select-window w) (switch-to-buffer "*compilation*") (shrink-window (- h compilation-window-height))))))) (add-hook 'compilation-mode-hook 'my-compilation-hook) (global-set-key (kbd "C-c C-c") 'comment-or-uncomment-region) (setq compilation-scroll-output t)
I’m not calling any of the goimports, gocode, etc as I used to, since
gopls takes care of it all.
Besides speed, there’s not a whole lot of difference between my previous and my current setups, really. The real advantage I see is that I’m future-proofing my setup by adopting officially-supported procedures. Just like going to Go Modules: my workflow is not improved, but this seems to be the direction the community is going and it will be easier for me to adopt it now than in the future.
The first time you open a
.go file, you will be prompted for the root of the project. It will find
go.mod and suggest that directory as the root. It’s an extra step, but not a big deal. The interface makes it simple enough.
This root path might have been the issue I initially hit where Emacs would color everything red on save. I couldn’t figure out what the problem was (I assumed it was a problem with my GOPATH and Modules), but removing my
.emacs.d directory and having it recreated fixed it on my Mac. I never had the problem on my Fedora laptop.