s10a's blog



Setting up Emacs for Golang

投稿日:   カテゴリー: tech

Table of Contents

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/bin to the $PATH environment 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.