I found that there were not many articles about setting up Emacs for Golang so I decided to write about one.
I am aware that tree-sitter and LSP features are available in latest Emacs 29.4, but I won't be using them here. For tree-sitter, I tried setting it up but it gave me weird highlighting result and ended up reverting back to normal non tree-sitter mode. For LSP, I am too annoyed of setting up multiple stuff and it gives me headaches so I'm not covering it in here. Maybe I will cover them at someday.
Note that I will not explain how to write Elisp and other stuff. I will only focus on setting up for programming in Golang. There are multiple ways to install package but I will be using use-package here.
Install Emacs
I believe you already have installed it but if you haven't yet, install it with your package manager. Some of the Linux distributions provide relatively old version of Emacs, so I suggest you to build Emacs from source in that case.
Install Golang
I also believe you have Golang environment but I will follow the official documentation to install, just in case if you haven't yet.
If you are using Linux, macOS or some kind of Unix like operating system follow the steps below. For Windows user, I'm sorry but I have nearly zero knowledge. Just download the installer and follow the instructions of it. Ignore below and skip to the "Installing godef and goimports" part.
Download the tar archive from download page . Make sure to download archive with appropriate operating system and architecture.
In my case I'm using Linux and my architecture is x86-64 so I
downloaded the following file using wget. Of-course, you can just do
right-click and save with your browser.
wget https://go.dev/dl/go1.23.3.linux-amd64.tar.gz
Compare the hash value to see if you have downloaded the correct file.
$ sha256sum go1.23.3.linux-amd64.tar.gz
a0afb9744c00648bafb1b90b4aba5bdb86f424f02f9275399ce0c20b93a2c3a8 go1.23.3.linux-amd64.tar.gz
If it does not return the same value listed in the download page, you probably have a broken file. Re-download and try again.
Now, extract the tar archive and move the extracted folder to
/usr/local directory.
tar -xzf go1.23.3.linux-amd64.tar.gz
sudo mv go /usr/local
Next, add /usr/local/go to the PATH environment variable. Put the
following to your ~/.profile file.
export PATH=$PATH:/usr/local/go/bin
The setup is complete and verify that you can run the go
command. Reopen the terminal and run go version command. You should
see it printing the version.
$ go version
go version go1.23.3 linux/amd64
Now we have go command available, we will also need some additional
setup. Set GOPATH environment variable. This variable will be used
to store source code or compiled binaries obtained with go install
command.
By default, this GOPATH value will be your home directory and it
will clutter your home directory which is kind of annoying. The value
can be anything but I recommend you to follow the XDG Base
Directory.
In my case, I am using ~/.local/share/go as value of GOPATH
environment variable.
Also, add $GOPATH/bin to the PATH environment variable to run
installed binaries. Put the following to your ~/.profile file.
export GOPATH=~/.local/share/go
export PATH=$PATH:$GOPATH/bin
Install goimports and godef
Install
goimports. goimports
will be used to auto-format your code and add missing imports or
remove unnecessary imports automatically.
go install golang.org/x/tools/cmd/goimports@latest
Install godef. godef will be
used to jump to definition of the code on given point.
go install github.com/rogpeppe/godef@latest
Let's verify that we have both goimports and godef installed.
$ command -v godef
/home/shuto/.local/share/go/bin/godef
$ command -v goimports
/home/shuto/.local/share/go/bin/goimports
Elisp code to integrate with Emacs
After we installed Golang, godef and goimports, we can finally
write some Elisp code for Emacs. Below is the entire code. You can
copy/paste this to your init.el. I will explain each bits later.
(use-package yasnippet
:bind
("C-x y" . yasnippet-insert-snippet))
(use-package yasnippet-snippets)
(use-package go-mode
:init
(setenv "PATH" (concat (getenv "PATH") ":" "/usr/local/go/bin"))
(add-to-list 'exec-path "/usr/local/go/bin")
(setenv "GOPATH" (expand-file-name "~/.local/share/go"))
(setenv "PATH" (concat (getenv "PATH") ":" (expand-file-name "~/.local/share/go/bin")))
(add-to-list 'exec-path (expand-file-name "~/.local/share/go/bin"))
:bind
(:map go-mode-map
("C-c C-e" . (lambda ()
(interactive)
(yas-expand-snippet (yas-lookup-snippet "error"))))))
(use-package apheleia
:config
;; prefer `goimports' instead of default `gofmt'
(when (executable-find "goimports")
(setf (alist-get 'go-mode apheleia-mode-alist)
'(goimports)))
:hook
(prog-mode . apheleia-mode))
Use yasnippet template system to expand some snippets. The yasnippet-snippets package contains the actual snippets to be used with yasnippet.
(use-package yasnippet
:bind
("C-x y" . yasnippet-insert-snippet))
(use-package yasnippet-snippets)
This part is our main
dish. go-mode is the major
mode we use when editing Golang source code. We add
/usr/local/go/bin and ~/.local/share/go/bin to exec-path
variable to run commands in Emacs. With setenv, it sets environment
variable for both PATH and GOPATH.
There are several keybinds already set. For example C-c C-a
(go-import-add) will add import to the import(...) part. Although
goimports can handle formatting and imports for standard libraries,
there are cases to manually add external package so keep it in
mind. C-c C-d (godef-describe) will be used to describe the
function which the cursor is on. It comes in handy to check the
argument and return values of the function. C-c C-j (godef-jump)
will jump to the defined source code using godef. This is useful
since it can jump instantly without searching the entire source code.
I only added this binding C-c C-e. It will expand the if err != nil snippet using yasnippet. I believe this is one of the most
frequently used code and setting a keybind for it was very useful for
me.
(use-package go-mode
:init
(setenv "PATH" (concat (getenv "PATH") ":" "/usr/local/go/bin"))
(add-to-list 'exec-path "/usr/local/go/bin")
(setenv "GOPATH" (expand-file-name "~/.local/share/go"))
(setenv "PATH" (concat (getenv "PATH") ":" (expand-file-name "~/.local/share/go/bin")))
(add-to-list 'exec-path (expand-file-name "~/.local/share/go/bin"))
:bind
(:map go-mode-map
("C-c C-e" . (lambda ()
(interactive)
(yas-expand-snippet (yas-lookup-snippet "error"))))))
The package apheleia is
used to auto-format files. Code formatting will run whenever you save
the file. Note that formatting will only trigger whenever the minor
mode apheleia-mode is on. (The global minor mode
apheleia-global-mode will also do the same)
The variable apheleia-formatters contains a list of defined
formatters with it's CLI command. The variable apheleia-mode-alist
decides what formatters to be used for each major mode. By default
aphelia uses gofmt formatter for go-mode. But, I found that using
goimports is more useful since it can also handle importing packages
while auto-formatting.
The below snippet uses apheleia and modifies to use the predefined
formatter goimports for go-mode whenever the executable called
goimports is found.
(use-package apheleia
:config
;; prefer `goimports' instead of default `gofmt'
(when (executable-find "goimports")
(setf (alist-get 'go-mode apheleia-mode-alist)
'(goimports)))
:hook
(prog-mode . apheleia-mode))
I believe this covers the basics of setting up Golang programming environment in Emacs. I hope you have fun with programming :)
- 2024-12-19: I forgot to add
$GOPATH/binto the$PATHenvironment variable in Emacs. Fixed elisp code to do so. - 2025-11-24: I migrated the reformatter package to aphelia. Also note that my setup is quite deviated from this one and I won't update this post in future. If you are interested in my up-to-date actual Emacs configuration, take a look at my dotfiles repository.