Blogs
Emacs for Go development from scratch
I recently helped one of my labmates set up a basic Emacs configuration for writing Go. While that was fresh in my mind, I figured I might as well write down the steps we took to get it up and running since it can be a little tricky to get all the pieces working. Using a Mac introduced additional complications for him, so things will hopefully go a bit better on Linux. For this tutorial, I spun up a fresh Debian 10 instance on Vultr to emulate a totally new Emacs user.
Installing Emacs
The first command I ran was
$ apt update && apt upgrade
to make sure my already-installed packages were up to date. Then, I ran
$ apt install emacs golang
to get the two programs I want to set up. Obviously the installation may differ on your platform. After finishing the installation I went ahead and opened Emacs to start configuring it. If you’ve never used Emacs before, I recommend at least reading over the tutorial and seeing if you like the default keybindings. I’m too used to Vim bindings, though, so the first thing I want is to install evil-mode. Before you can install packages, you need to tell Emacs where the packages are. According to the Emacs Wiki, ELPA is already loaded by default, but many other packages are in MELPA. We can tell Emacs to also find packages in MELPA by adding the commands
(require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) (package-initialize)
to your config file. By default Emacs should create the directory
~/.emacs.d
, and you can name your
file init.el
in that directory. To “find” a file in
Emacs, which is how to open a file, type
Ctrl+x
followed by Ctrl+f
. In Emacs this
kind of binding is typically abbreviated C-x
C-f
. Similarly, the Meta key is typically abbreviated
as M-
, which can be confusing since on most keyboards
the Alt key will correspond to Meta.
S-
is sometimes used for Shift as well.
Once you have that loaded into your init file, you can either run
the individual Lisp commands by going to the rightmost parenthesis of
each expression and typing
C-x C-e
to evaluate the expression, or you can reload
the whole file by typing
M-x load-file
and then the path to your init file when
prompted. With MELPA added, we now need to refresh the list of
packages with
M-x package-refresh-contents
. On Debian 10, which
installed Emacs 26.1 by default, I kept getting an error about
failing to load the GNU package archive. According to
this
Reddit post, which in turn cites some other sources, you can fix
this problem by adding the command
(setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3")
before you try to refresh packages. If you use a more recent version of Emacs, I think this should not be a problem.
In debugging that, I also realized that Emacs created a
~/.emacs
file separate from my init file where I was
writing my configuration. If that happens to you, an easy fix is to
open
~/.emacs
and add a line loading your intended config
file such as
(load "~/.emacs.d/init.el")
as I added to mine.
Now that the package list is up to date, there are actually a
couple of options. You can run
M-x package-list-packages
, search for evil with
C-s evil
and then click to install evil, but the more
convenient approach is to do
M-x package-install RET evil
, where RET is the return
key used to finish the first part of the command. You will also
press Return after typing evil, but that should be more
obvious. Once you have
evil
installed, add the following lines to your init
file and it will be enabled by default.
(require 'evil) (evil-mode 1)
Reload your init file and breathe a sigh of relief at having your normal Vim keys for the rest of this process.
use-package
After getting evil installed, I usually switch to using the package
use-package
for handling my package installation needs
since that effectively keeps a nice list of installed packages for
me in my init file and theoretically would let me get set up
basically instantly on a new machine. I install
use-package
just like
evil
, using the command
M-x package-install RET use-package
. At this time I
also enabled a theme to match my local installation of Emacs. You
can do that with the command
(load-theme 'adwaita)
I’m still a big fan of the
tango-dark
theme, but I’ve been experimenting with
light themes lately. I like the default theme for the most part, but
my evil visual selection is totally invisible against it, so I’ve
been using
adwaita
. Anyway, back to
use-package
. Now that we have that installed, we can
start using some more packages. One particularly useful one is
helm
because it offers fuzzy searching of basically
everything in Emacs instead of the built-in tab completion. For some
things like finding files I actually much prefer the built-in
completion, but for switching buffers I love
helm
since you don’t have to type
*M<tab>
to get to the
*Messages*
buffer, for example. A basic pattern for
use-package looks like
(use-package helm :ensure t :config (helm-mode 1))
where the package name comes after
use-package
, the
:ensure t
keyword means ensure the package is
installed, and
:config
starts configuration associated with that
package. We’ll use
use-package
like this to install the rest of the
packages we need.
Now if you’ve been using
evil
mode and you’ve had the misfortune of trying to
redo something, you’ll get a message like
user-error: Customize ‘evil-undo-system’ for redo
functionality.
in the minibuffer.
evil
used to require
undo-tree
on installation, but apparently bleeding-edge
versions of Emacs have this kind of functionality built in. That’s
great for people on Emacs 28 already, but the rest of us now have to
install a separate undo package and then configure it. Since it’s
the one I’m familiar with, we’ll just use
undo-tree
:
(use-package undo-tree :ensure t :config (evil-set-undo-system 'undo-tree) (global-undo-tree-mode))
With this, undo and redo should be nearly as seamless as in Vim itself. We should now be able to move on to writing some Go.
Getting Go to Compile
I now hit
C-x C-f
to find a file called
hello.go
and filled in a minimal hello world example. I
immediately noticed that there was no syntax highlighting and that I
was in Fundamental mode. To fix that we can add
(use-package go-mode :ensure t)
to our init file, where I have omitted the
:config
since I don’t have any additional configuration
to add. Now I go back to my hello.go buffer and run
M-x go-mode
to enable my new Go mode and I now have
syntax highlighting.
The next thing to test is whether or not I can actually compile
Go. Of course I could use a Makefile or something and let the shell
figure out where everything is, but I’d like to use the built-in
M-x compile
with a compile command of
go run .
or
go test .
instead. I thought I remembered that not
working out of the box, but surprisingly my Emacs path was already
updated with my Go installation. Maybe that was a Mac problem.
The base Go mode already has some very handy features such as
adding imports with
C-c C-a
, which we can also search incrementally since
we have helm installed. It also comes with support for
M-x gofmt
out of the box. We can make
gofmt
a little easier to run though by defining a local
key binding. I typically bind recompile to F5, so I like having
gofmt
nearby on F6. However, on my 60% mechanical
keyboard, I don’t actually have function keys, so I also bind these
to
C-c 5
and
C-c 6
, respectively. To make that happen, you can add
these expressions to your init file:
(add-hook 'go-mode-hook (lambda () (local-set-key (kbd "") 'gofmt))) (add-hook 'go-mode-hook (lambda () (local-set-key (kbd "C-c 6") 'gofmt)))
You will need to reload Go mode for those to take effect though since we defined them as hooks.
The main things we are missing now are auto-completion, which we’ll
get from
gocode
and
company-mode
, better documentation reference, which
we’ll get from
godoc
and
go-mode
itself, and smarter import handling, which
we’ll get by installing
goimports
and configuring
go-mode
to run that instead of normal
gofmt
. We’ll start with the last of these since it’s
the easiest and will actually test out many of the settings we need
for the others.
goimports
If you’re new to Go, you may not know how easy it is to install packages. Run this command to install goimports:
$ go get golang.org/x/tools/cmd/goimports
and then the hard part is just getting Emacs to find that in your
path. Apparently you need to have
git
installed for
go get
to work, which is unlikely to be a problem for
most people, but I had to
apt install git
at this point before I could actually
run the command above. As it turns out, it’s not actually that hard
to get the path to match up either, at least on Linux. Again, I
think this took more effort when on a Mac, but all I had to add to
my
:config
section of
go-mode
was
(add-to-list 'exec-path "/root/go/bin") (setq gofmt-command "goimports")
and I was ready to go, as it were. What we’re adding to path is
really
$GOPATH/bin
, which will likely be more useful in your
case. Since this is just a temporary machine, I haven’t set up the
GOPATH
environment variable, and I installed everything
as root, so adjust the directory as needed to actually align with
where Go installs these executables. Off the top of my head, I think
getenv
and
setenv
are the useful functions for setting this up
with your environment variables, where you can do something like
this to update the path in Emacs with your GOPATH:
(setenv "PATH" (concat (getenv "PATH") (getenv "GOPATH")))
If you run into errors where things are not on your path as you think they should be, it’s probably because Emacs evaluates its path a bit differently from your shell, and you need to run something like this to update it. However, I did not test these for this post.
godoc and godef
In the same vein, we can now
go get godoc
and our path should include that as well,
so it should work immediately. As it turns out, it’s not exactly
go get godoc
. The command for most people will probably
be
$ go get golang.org/x/tools/cmd/godoc
but on Debian stable my Go version was too far out of date, so I had to add
deb http://deb.debian.org/debian buster-backports main
to my
/etc/apt/sources.list
file, run
apt update
, and then
$ apt -t buster-backports install golang
to get a more recent version of Go that was compatible with that
version of
godoc
. You can then use the command
M-x godoc-at-point
to view the documentation for
whatever is under your cursor in another buffer.
godoc
is also nice for viewing documentation offline in
your browser, but that’s not really related to this tutorial. To
make it more convenient than typing the command, I bind this to
C-c d
using another hook to keep the binding local to
go-mode.
(add-hook 'go-mode-hook (lambda () (local-set-key (kbd "C-c d") 'godoc-at-point)))
That other buffer popping up can also be a bit annoying and is
often more information than you really need.
go-mode
comes with a binding already built-in for
godef
which gives you a brief description of the
function in the minibuffer with
C-c C-d
. To install
godef
just run
$ go get github.com/rogpeppe/godef
and that binding should start working right away.
Completion with company-mode
These niceties are useful, but the big thing we need is
auto-completion. There are a few frameworks for this in Emacs, but the
one I’ve always used is
company-mode
. My understanding of
company-mode
is that it provides the infrastructure for
you to slot in different backends for the languages you want to
use. As such, we need to install both
company-mode
itself, and the Go backend
company-go
. You can enable
company-mode
globally if you want, but I actually find
it really annoying in text files and especially if you ever use the
shell within Emacs, so I just install it and then enable it in the
modes I really want it in. This is partly because of the config
lines here that set the delay for it to start suggesting things to
0.1 seconds and the amount you have to type to 0 characters. This
means that it is constantly suggesting things. You can make either
of those larger if you want of course, and that will help to make it
a bit less overbearing if you find it that way with my settings.
(use-package company :ensure t :config (setq company-idle-delay 0.1) (setq company-minimum-prefix-length 0)) (use-package company-go :ensure t)
company-go
in turn uses
gocode
as its backend for actually generating
completions, so without it the mode is not very useful. In fact,
without the
gocode
executable,
company-go
is actually worse than plain
company-mode
since it throws an error instead of just
offering very weak completion. To fix this, run
$ go get github.com/mdempsky/gocode
to obtain
gocode
and then try typing in your Go-mode buffer. It
should be up and running! I originally found the default
company-mode
set up really nice, but they updated
somewhat recently and it became totally unintuitive, so I also add
the following configuration to keep it from typing text in as a
template and to change the key to cycle through the options to Tab.
(setq company-go-insert-arguments nil) (company-tng-configure-default)
I guess the first of these is actually a company-go update, but
still I prefer to get rid of them both. It looks like the default
colors used in
company-mode
are actually pretty nice on a light theme,
but since I changed them to go with
tango-dark
, I’ll include the code for that in case you
want to play with it too:
(custom-set-faces '(company-preview ((t (:foreground "darkgray" :underline t)))) '(company-preview-common ((t (:inherit company-preview)))) '(company-tooltip ((t (:background "lightgray" :foreground "black")))) '(company-tooltip-selection ((t (:background "steelblue" :foreground "white")))) '(company-tooltip-common ((((type x)) (:inherit company-tooltip :weight bold)) (t (:inherit company-tooltip)))) '(company-tooltip-common-selection ((((type x)) (:inherit company-tooltip-selection :weight bold)) (t (:inherit company-tooltip-selection)))))
Conclusion
Overall that felt like a pretty painless experience, even with some minor version issues on Debian. As a warning, I do not think this went nearly as smoothly on Mac, but if you are using Linux it should be just as smooth if not smoother. Down below I’ll put the full pieces of code for very quick copy and pasting of basically the whole process, but if you run into any problems please let me know! I have been meaning to write something like this for a while, and I actually had LaTeX in mind as the language to cover, but Go came up first in a real situation. I will probably write a follow-up extending this config to writing LaTeX in Emacs as well in the near future.
Snippets
# install default emacs and go versions on a debian-based distro $ sudo apt install emacs golang # install the go packages we need $ go get golang.org/x/tools/cmd/goimports \ golang.org/x/tools/cmd/godoc \ github.com/rogpeppe/godef \ github.com/mdempsky/gocode
;; the whole config file I ended up with (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3") (require 'package) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) (package-initialize) ;; you can also add the following to ensure evil mode is installed ;; without having to M-x package-install: (unless (package-installed-p 'evil) (package-install 'evil)) ;; and I assume the same is true for use-package (unless (package-installed-p 'use-package) (package-install 'use-package)) ;; now the whole process should be covered by loading this file (require 'evil) (evil-mode 1) (load-theme 'adwaita) (use-package helm :ensure t :config (helm-mode 1)) (use-package undo-tree :ensure t :config (evil-set-undo-system 'undo-tree) (global-undo-tree-mode)) (use-package go-mode :ensure t :config (add-to-list 'exec-path "/root/go/bin") (setq gofmt-command "goimports") (add-hook 'go-mode-hook (lambda () (local-set-key (kbd "C-c d") 'godoc-at-point))) (add-hook 'go-mode-hook (lambda () (local-set-key (kbd "C-c 5") 'recompile))) (add-hook 'go-mode-hook (lambda () (local-set-key (kbd "C-c 6") 'gofmt)))) (use-package company :ensure t :config (company-tng-configure-default) (setq company-go-insert-arguments nil) (setq company-idle-delay 0) (setq company-minimum-prefix-length 0)) (use-package company-go :ensure t :config (add-hook 'go-mode-hook (lambda () (set (make-local-variable 'company-backends) '(company-go)) (company-mode))))