Setting up a Go development environment in Neovim

vimgolang

LSP Client Configuration (nvim-lspconfig)

Install gopls, LSP server for Go.

$ go install golang.org/x/tools/gopls@latest
$ gopls version
golang.org/x/tools/gopls v0.16.2

Since Neovim has a built-in LSP client, it works by simply passing the gopls configuration from nvim-lspconfig.

Set it up to apply formatting on save.

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = "*.go",
  callback = function()
    local params = vim.lsp.util.make_range_params()
    params.context = {only = {"source.organizeImports"}}
    -- buf_request_sync defaults to a 1000ms timeout. Depending on your
    -- machine and codebase, you may want longer. Add an additional
    -- argument after params if you find that you have to write the file
    -- twice for changes to be saved.
    -- E.g., vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 3000)
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params)
    for cid, res in pairs(result or {}) do
      for _, r in pairs(res.result or {}) do
        if r.edit then
          local enc = (vim.lsp.get_client_by_id(cid) or {}).offset_encoding or "utf-16"
          vim.lsp.util.apply_workspace_edit(r.edit, enc)
        end
      end
    end
    vim.lsp.buf.format({async = false})
  end
})

Set up keymaps for actions like jumping to definitions. C-o to go back and C-i to go forward.

vim.keymap.set('n', 'K', vim.lsp.buf.hover, { desc = 'Show hover' })
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = 'Go to definition' })
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, { desc = 'Go to implementation'})
vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts, { desc = 'Show references' })

Completion (nvim-cmp)

While completion is possible with LSP alone C-x C-o, using nvim-cmp enables automatic completion with godocs displayed alongside. It also supports completion from other sources such as paths.

Install the nvim plugin manager lazy.nvim and add plugins - sambaiz-net

{
	{
    'hrsh7th/nvim-cmp',
    config = function()
      local cmp = require('cmp')
      cmp.setup({
        mapping = cmp.mapping.preset.cmdline(),
        sources = {
          { name = 'nvim_lsp' },
          { name = 'buffer' },
          { name = 'path' },
        },
        mapping = cmp.mapping.preset.insert({
          ['<C-b>'] = cmp.mapping.scroll_docs(-4),
          ['<C-f>'] = cmp.mapping.scroll_docs(4),
          ['<C-Space>'] = cmp.mapping.complete(),
          ['<C-e>'] = cmp.mapping.abort(),
          ['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
        }),
      })
    end
  },
  {
    'hrsh7th/cmp-nvim-lsp',
    dependencies = {
      'neovim/nvim-lspconfig',
    },
    config = function()
      local lspconfig = require("lspconfig")
      local capabilities = require('cmp_nvim_lsp').default_capabilities()
      lspconfig.gopls.setup({
          capabilities = capabilities,
      })
    end
  },
  {
    'hrsh7th/cmp-buffer',
  },
  {
    'hrsh7th/cmp-path',
  },
  {
    'hrsh7th/cmp-cmdline',
  }
}

syntax highlight (nvim-treesitter)

Installing nvim-treesitter, which is used for tree-sitter that parses code and creates syntax trees, makes syntax highlighting complete.

{
    'nvim-treesitter/nvim-treesitter', tag = 'v0.9.3',
    config = function()
      require('nvim-treesitter.configs').setup({
        ensure_installed = { 'go', 'lua', 'typescript', 'javascript', 'json', 'yaml', 'html', 'css', 'bash', 'python', 'markdown', 'scala' },
        highlight = {
          enable = true,
        },
      })
    end
  }

Test Execution and Coverage Visualization (neotest, nvim-coverage)

Install neotest and the Go runner neotest-go to run tests.

{
  'nvim-neotest/neotest', tag = 'v5.6.1',
  dependencies = {
      'nvim-neotest/nvim-nio',
      'nvim-lua/plenary.nvim',
      'antoinemadec/FixCursorHold.nvim',
      'nvim-treesitter/nvim-treesitter',
      'nvim-neotest/neotest-go',
  },
  config = function()
      require('neotest').setup({
        adapters = {
          require('neotest-go')({
            args = { '-coverprofile=coverage.out' },
          })
        },
      })
  end,
}

vim.keymap.set('n', '<Leader>tt', ':lua require("neotest").run.run()<CR>', { desc = 'Run test' })

While the status icons were garbled in the standard terminal, they were displayed correctly in WezTerm.

The output coverage profiles can be visualized with nvim-coverage.

{
	'andythigpen/nvim-coverage',
	dependencies = {
	    'nvim-lua/plenary.nvim',
	},
	config = function()
	    require('coverage').setup({
	      lang = {
	        go = {
	          coverage_file = vim.fn.getcwd() .. '/coverage.out',
	        }
	      }
	    })
	end,
}

The color of the bars displayed with :Coverage indicates whether the code has been tested or not.