Improving my Everyday Developer Experience

The Story of Tuning my Neovim Config for Go Development

Improving my Everyday Developer Experience

My Preferred Development Environment

For about a year I have used my basic Neovim (nvim) config for learning and developing in Go. This is the same config I have used for many years, developing in Python, C, C++, Javascript, Typescript, and Bash. For those of you who are familiar with vim this shouldn't be all that surprising, it pretty much just works most of the time. Especially when used in conjunction with other terminal-based tools such as git, make, grep (or ripgrep in my case), sed, tree, your choice of compiler/linter/test-runner/static analysis, etc.

This environment is where I most often find my flow state.

NOTE: I use nvim over vim primarily because I jump back and forth between Mac OS, Linux, and Windows and I've had a much better experience getting it to work the same on all those systems (looking at you Windows). Additionally, Neovim uses more sane defaults, has better performance with plugins, has been aggressively refactored and modernized, and is maintained by a community rather than an individual. If you're interested, you can learn more here.

The Eventual Friction

Regardless of how much I love my workflow, as my codebases grow in size and complexity, I always find myself wanting a few more features integrated into my development environment. Things such as;

  • go-to-definition
  • View all references of a given variable or function
  • View a method signature or docs for a package I'm not familiar with
  • Sane renaming and refactoring
  • Debugging
  • etc.

Each time this happens I usually spend a week or so researching different IDEs (usually starting with JetBrains products if I'm being honest) and comparing their features. I will try a few but always get frustrated by different things;

  • Learning new keyboard shortcuts
  • Conforming to a new build structure (although makefile integration is becoming more universally available)
  • Not being able to easily use external tools with files open in the IDE
  • How slow it is to launch a full IDE
  • The inability to just edit a single file not part of a project
  • etc.

These frustrations usually outweigh my desire for more IDE-like features and I will simply return to the comfort and productivity of nvim and the terminal.

A Better Way

This time around, with Go, rather than trying to learn something new, like Goland, I decided to focus on improving the developer experience of my current tools to achieve the same goal in an environment I'm already proficient in. My search led me to quite a few different tools, plugins, articles, and config tweaks. I'm still playing with some of them, but I'd like to document my experiences so far, and where I've landed for now.

While the rest of this article will be entirely focused on the technical details of configuring nvim to be a good development environment for Go, there is a bigger takeaway;

As developers, our tools do not need to be frustrating!

Call To Action

My development environment has been something I've been fighting with on and off for years. And, when it was all said and done, the process documented below ended up taking me a couple of afternoons to get comfortable with. Well worth my time.

Overall, I think we could all benefit from occasionally stepping back from the pressure we feel to always "be productive", especially in sub-optimal situations. Most of the time, it will be much better for us, in the long run, to slow down a little and focus instead on having good experiences with the tools and systems we use every day.

While nvim is the focus of the rest of this article, I know it is not for everyone. I would challenge you, whatever your tools of choice, to take some time this week to take stock of your tools, systems, and overall developer experiences. Then identify where there is friction and make a plan to try and overcome it.


Ok, without further ado, here is how I have configured Neovim for Go development to improve my daily developer experience.

Ok sorry, one little smidgen more ado...

If you haven't used Neovim before, this is probably not going to be a great use of your time beyond this point. Unless you're considering it and want to see what Go development looks like in Neovim, then great!

The same goes for Go.

I won't be introducing the basics of either Neovim or Go in this article. If that's where you are in your journey; Welcome! Here are a few places to get familiar with the basics. Once you've read up on those, hop on back over here and pick up where you left off. And don't hesitate to reach out with any questions you may have.

Vim Basics - Neovim - Go

Ok, for reals this time, here we go.

vim-go, The Holy Grail

The most essential plug-in you can have for Go development is hands down vim-go.

This single plug-in bundles together so much functionality that you really don't need much else. And it is super easy to install (unlike some of the other more "advanced" plug-ins I have tried). It was written by Fatih, is still in active development, and has been contributed to by over 320 people.

I'm using vim-plug as my nvim package manager, so I installed vim-go by adding the following to the Plug section of my init.vim.

Plug 'fatih/vim-go', { 'do': ':GoUpdateBinaries' }

Check here for further instructions if you use a different package manager.

Once you have added that to your init.vim, run the following from the command line to download and install the vim-go plug-in.

nvim +PlugInstall +qall

This could take a moment because it will also reach out and install all the additional Go binaries it needs via go get. You can check what was installed by looking in your $GO_PATH/bin/ directory, but for the most part, you won't need to worry about it since vim-go takes care of all the interactions with those tools for you.

You might need to run the following command if you get errors in nvim after trying to invoke any of the vim-go functionality. (For some reason Plug occasionally failes to run :GoUpdateBinaries upon install, so this command fixes that)

nvim +GoInstallBinaries +qall

So, what new abilities did we just unlock? A metric-f-ton!

I'll briefly go over the top 15 that I have found the most useful so far. Then at the end of the article, we'll dive into the init.vim config I used to better surface these abilities while writing code.

vim-go, Unlocked Abilities

  1. Auto package importing/dropping

    • When you save a source file, any packages that you added/removed references to will be automatically imported/dropped from the import block at the top of the file. (No more "package imported but not used" nonsense)
  2. Auto source code formatting

    • When you save a source file, go fmt is automatically run. (No more committing files with inconsistent formatting)
  3. Goto definition/declaration

    • nvim's built-in commands (gd or C-]) work for jumping to definitions/declarations of functions, types, and variables. (Use C-o or C-t to jump back)
  4. See all references

    • You can pull up a list of all the places where the selected function, variable, type, or channel is referenced with :GoReferrers or :GoChannelPeers. (And use that list to jump around to those locations)
    • Bonus: You can pull up the callstack of the cursor at any place in the source file with :GoCallstack.
  5. Peek at definition information

    • You can quickly see the signature of methods/functions, definitions of type, fields of struct, and more with :GoInfo and :GoDescribe. (No more jumping back and forth between definition and usage)
    • Bonus: You can pull up a list of all the interfaces a selected type/struct implements with :GoImplements.
  6. Jump to Go Docs

    • You can either open the Go Docs right inside nvim with :GoDoc or have it open the same page up in your default web browser with :GoDocBrowser. (This saves me so much time while looking things up!)
  7. Type-save, AST aware, renaming

    • Rename variables, types, functions, etc in a safe way with :GoRename. (Like sed, but without the unintentional changes)
  8. Identify Freevars for function extraction

    • Given a selected block of code you can pull up a list of the free variables, aka the variables that would need to be passed into a function containing only the selected lines of code. You do this with the command :GoFreevars; This has to be done on a visual selection so it will look like :'<,'>GoFreevars. (This is great for refactoring complex sections of code)
  9. Auto json struct tagging

    • Removes the tedium of adding json tags to every field in your structs with :GoAddTags. (This does wonders for your sanity)
    • Bonus: Can be configured for either snake_case or `camelCase to match your code's conventions (see below).
  10. Build, Install, Test, and Run without leaving Neovim

    • Honestly, I only really ever use go build from inside nvim to make sure I haven't broken anything that would prevent it from compiling. I generally have a Makefile that orchestrates the others.
    • The commands for these, as you can probably guess, are :GoBuild, :GoInstall, :GoTest and :GoRun.
  11. Auto Linting and Vetting

    • When you save a source file it can automatically make idiomatic suggestions by running lint and vet on the file. (Good stuff to catch before committing the file)
    • Their commands to run manually are :GoLint and :GoVet. Or you can create a collections via your init.vim and run them all via :GoMetaLinter.
  12. Listing all package files and dependencies

    • You can pull up lists of all the files (:GoFiles) actually being used in the compiles package as well as all the external/internal dependencies (:GoDeps) the package has.
  13. nvim recognizes Go functions

    • In and Around motions make it super quick to move, copy, or delete entire functions or functions bodies.
    • You can use [[ and ]] to jump around a source file by functions
  14. Autocomplete (think Intellisense)

    • Since vim-go installed gocode and gopls we can use nvim's built-in autocomplete functionality with all internal/external go packages.
    • Anytime you want intelligent autocomplete stop typing and press ctrl-x-o and a popup will appear with everything that could follow what you've typed. You can navigate that windows using ctrl-n (next) and ctrl-p (previous), and insert your selection with ctrl-y (yup!)
  15. Code sharing

    • You can use :GoPlay to open a file or selected area in The Go Playgound. (Honestly, this one is just more fun than anything, but is occasionally helpful when I want to get some quick feedback on a small part of the code I haven't commit yet.)

Honorable Mentions

Here are a few other plug-ins that I've found useful. They aren't specifically for Go development, but they have improved my development experience overall.

NERDTree

NERDTree is basically a side bar file explorer like you see in most IDEs. This plug-in is pretty simple and just works, not much else to say about that. I am considering removing this plug-in however in favor of the built-in version, Netrw. Nothing wrong with NERDTree, just a way to reduce my config bloat and dependencies a bit.

vim-gitgutter

vim-gitgutter shows git status is the sidebar (by the line numbers). This plug-in does a whole lot more than that, but primarily I use it to see the status of the file I'm working on and which parts are new in a given commit.

Tmux Fixes

There are 3 plug-ins I use to help enhance the experience of using nvim inside of tmux. These fix some common pain points as well as make navigation between vim and tmux panes seamless. They are;

SplitJoin

This one I'm still on the fence about how necessary it really is. SplitJoin basically helps with splitting single-line struct expressions into multiple lines with gS or with joining a multi-line struct expressions to a single line with gJ. This works with more than just structs, but that's primarily where I use it. Again, not really sure I need to install a separate plug-in just for this, but it's an interesting idea nonetheless.

I'm Still Experimenting

There are a few other plug-ins that I've been wanting to try but haven't got around to yet. So will I won't be recommending or reviewing these here I did want to list them so you can check them out too if you're interested.

Git Tools

  • tpope/vim-fugitive
    • Primarily interested in this one for the git blame functionality.
  • vim-gitgutter
    • I wan to explore the additional features in this one I'm not using yet.

Fuzzy Finding

Debugging

  • go-delve
    • This one has integrations with vim-go, just haven't tried it out yet

Snippets

  • SirVer/ultisnips
    • This one also is supposed to have good integration with vim-go, but I've never really been into using snippets, regardless of the IDE

Configuration

Ok, the moment everyone has been waiting for... The GitHub link to my init.vim config. Now you can copy-pasta it and forget everything I've said above, while simultaneously becoming enraged that it's "broken". 😉 It's ok we've all been there.

But really, I hope this has been helpful so far. Not only how to configure nvim for Go development, but I hope you've been inspired to take some time to consider the tools you're using and how they affect your everyday developer experience.

Here are the links;

I also wanted to briefly call out the important parts that are relevant to this article;

" ~~~ GO SPECIFIC ~~~
" ~~~~~~~~~~~~~~~~~~~
augroup go
    au FileType go set noexpandtab
    " Map :GoBuild to ,b "
    autocmd FileType go nmap <leader>b  <Plug>(go-build)
    " Map :GoRename to ,r "
    autocmd FileType go nmap <leader>r <Plug>(go-rename)
    " Map :GoInfo to ,I "
    autocmd FileType go nmap <leader>i <Plug>(go-info)
    " Map :GoImplements to ,I "
    autocmd FileType go nmap <leader>I <Plug>(go-implements)
    " Map :GoDef (vertically) to ,d "
    autocmd FileType go nmap <leader>d <Plug>(go-def-vertical)
    " Map :GoDescribe to ,D "
    autocmd FileType go nmap <leader>D <Plug>(go-describe)
    " Map :GoDoc & :GoDocBrowser to ,? and ,?? respectively "
    autocmd FileType go nmap <leader>? <Plug>(go-doc)
    autocmd FileType go nmap <leader>?? <Plug>(go-doc-browser)
    " Map :GoReferrers to ,c "
    autocmd FileType go nmap <leader>c <Plug>(go-referrers)
    " Map :GoCallstack to ,C "
    autocmd FileType go nmap <leader>C <Plug>(go-callstack)
    " Map :GoChannelPeers to ,p "
    autocmd FileType go nmap <leader>p <Plug>(go-channel-peers)
    " Map :GoFreevars to ,f "
    autocmd FileType go vmap <leader>f <Plug>(go-freevars)
augroup END

" These clean up the save formatting and error reporting features "
let g:go_fmt_command = "goimports"
let g:go_list_type = "quickfix"
let g:go_addtags_transform = "camelcase" "At work our json schemas use camelcase, you can also set this to 'snakecase' if you want "

" This highlights types (e.g. primitives, stucts, custom types, etc.) "
let g:go_highlight_types = 1

" This highlights other instances of the variable under the cursor (I disable this because  I don't need it, but you might like it) "
let g:go_auto_sameids = 1

" Easier Quickfix Menu Navigation "
map <C-Right> :cnext<CR>
map <C-Left> :cprevious<CR>
map <C-Down> :cclose<CR>
map <C-Up> :copen<CR>

Cheatsheet

Neovim Cheat Sheet 1920x1080.png

You can grab a free copy of my Neovim Cheat Sheet at whichever shop you prefer.

Neovim Cheat Sheet at Ko-Fi

Neovim Cheat Sheet at Gumroad

References

Here are some of the other blog posts that aren't mentioned above. I used these while learning about configuring all this for myself.