Programming Go in Neovim

There are tons of articles on how to programming Go in vim, how to turn vim into IDE. The purpose of this article is to look closer at nvim as an LSP client, especially for Go.



Nvim introduced nvim-lspconfig, a collection of common configurations for Neovim’s built-in language server client . From that point nvim can be lsp client for any server that supports LSP specification.

My primary setup before was vim with vim-go. After Go Modules were introduced some of the dependencies stopped working. vim-go depends on a bunch of tools. Support of these tools has relied on the goodwill of community members, and they have been put under a large burden of support at times as the language, toolchain, and environments change. As a result, many tools have ceased to work, have had support problems, or become confusing with forks and replacements, or provided an experience that is not as good as it could be.

Moreover, I want to have the same experience with other languages without wasting time exploring new plugins and tools.

That’s how combination nvim + gopls appeared on my radar.


To use the new (still experimental) native LSP client in Neovim, make sure you install the prerelease v0.5.0 version of Neovim (aka “nightly”), the nvim-lspconfig configuration helper plugin, and check the gopls configuration section there.

Migration from Vim-go

Vim-go was my only setup for development Go in vim and my only setup for development in vim at all. That’s why I am so interested to set up a new Go development environment. Minimal configuration:

├── init.vim
└── lua
    └── lsp_config.lua

Let’s take vim-go tutorial to make a comparision:

go get

Run, Build, Test, Cover it

For all this action I currently use command line and Go tools and haven’t found analogs in nvim lsp.

To be honest, the only feature I really from the list is test capabilities.

Fix it

Let’s introduce two errors by adding two compile errors:

var b = foo()

func main() {
	fmt.Println("Hello GopherCon")

fix it img

Here nvim enters the battlefield. We immediately get error messages from diagnostics without build.

You see available diagnostics and can navigate it back and forth.

  buf_set_keymap('n', '[d', '<cmd>lua vim.lsp.diagnostic.goto_prev()<CR>', opts)
  buf_set_keymap('n', ']d', '<cmd>lua vim.lsp.diagnostic.goto_next()<CR>', opts)

I find diagnostics features easy to use:

  • no additional action(build)
  • no additional window - errors are shown in the code file

Edit it

Format Let us start with a unformatted file:

package main

     import "fmt"

func main() {
 fmt.Println("gopher"     )

format img

To format it by shortcut you need to have key binding in your lsp config:

-- Set some keybinds conditional on server capabilities
if client.resolved_capabilities.document_formatting then
    buf_set_keymap("n", "ff", "<cmd>lua vim.lsp.buf.formatting()<CR>", opts)
  elseif client.resolved_capabilities.document_range_formatting then
    buf_set_keymap("n", "ff", "<cmd>lua vim.lsp.buf.range_formatting()<CR>", opts)

To format it on save file:

autocmd BufWritePre *.go lua vim.lsp.buf.formatting()


Let’s print the “gopher” string in all uppercase. For it we’re going to use the strings package. Change the definition to:


You will see error undeclared name: strings shown by diagnostics

code action image

You need strings in your imports and there is two ways to solve it: code action triggered manually

Code action:

buf_set_keymap('n', 'ga', '<Cmd>lua vim.lsp.buf.code_action()<CR>', opts)

and on save

 function goimports(timeoutms)
    local context = { source = { organizeImports = true } }
    vim.validate { context = { context, "t", true } }

    local params = vim.lsp.util.make_range_params()
    params.context = context

    -- See the implementation of the textDocument/codeAction callback
    -- (lua/vim/lsp/handler.lua) for how to do this properly.
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, timeout_ms)
    if not result or next(result) == nil then return end
    local actions = result[1].result
    if not actions then return end
    local action = actions[1]

    -- textDocument/codeAction can return either Command[] or CodeAction[]. If it
    -- is a CodeAction, it can have either an edit, a command or both. Edits
    -- should be executed first.
    if action.edit or type(action.command) == "table" then
      if action.edit then
      if type(action.command) == "table" then


autocmd BufWritePre *.go lua goimports(1000)


This feature is a bit complicated in configuration. You can configure snippets in a usual way snippets engine(UltiSnips) and snippets itself(honza/vim-snippets).

But LPS support there even more snippets options available. Gopls has a bunch of experimental features and experimental postfix completions is one of them.

Setup requires additional effort. First, you have to set your LSP config in a way that informs LSP server that you have snippet support. So when the server run it knows about your capabilities:

local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true

	capabilities = capabilities,

It will be visible from logs with

:lua vim.cmd('e'..vim.lsp.get_log_path())


lua << EOF



capabilities: {
   "textDocument ="{
      "completion ="{
         "completionItem ="{
            "snippetSupport = true",

postfix snippets

Check it

Gopls can publish not only errors, but warnings as well. Possible warning depends on analyzers we specify. For example, shadow analyzer:

	    settings = {
	      gopls = {
		     analyses = {
		    	shadow = true,

shadow img

A lot of analyzer are still experiment but I enjoy to use it - it allows you do not postpone to linter execution step

Probably most important feature set for development along with code completion.

go to definition

Nvim as LSP client supports next type of navigation request:


Key binding for some of them

  buf_set_keymap('n', 'gD', '<Cmd>lua vim.lsp.buf.declaration()<CR>', opts)
  buf_set_keymap('n', 'gd', '<Cmd>lua vim.lsp.buf.definition()<CR>', opts)
  buf_set_keymap('n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
  buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
  buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)


Completion is available out of the box. We just have to map it to vim omnifunc to make our Ctrl+x,Ctrl+o work:

  buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')


Understand it

Documentation lookup nvim supports as LSP client:


Key binding for some of them

  buf_set_keymap('n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
  buf_set_keymap('n', 'K', '<Cmd>lua vim.lsp.buf.hover()<CR>', opts)

  -- Set autocommands conditional on server_capabilities
  if client.resolved_capabilities.document_highlight then
      hi LspReferenceRead cterm=bold ctermbg=red guibg=LightYellow
      hi LspReferenceText cterm=bold ctermbg=red guibg=LightYellow
      hi LspReferenceWrite cterm=bold ctermbg=red guibg=LightYellow
      augroup lsp_document_highlight
        autocmd! * <buffer>
        autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
        autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
      augroup END
    ]], false)

hover, signature help and document highlight

Refactor it

Rename identifiers From nvim documentaton among LSP requests/notifications defined by default:


Key binding for it

  buf_set_keymap('n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)



After exploring nvim lsp capabilities I understood how mature the tool vim-go is. It perfectly covers Go development workflow.

At the same time, nvim as an LSP client looks solid and gopls as server matches it pretty smooth. Despite nvim with lsp support is in a release version and a lot of gopls features are experimental it is already advanced Go development environment and we can expect more in the future.

When it comes to basic setup, nvim requires just one plugin - nvim-lspconfig and using gopls for it requires just one config line.