Today we will introduce you how to configure Go development environment under Vim. If you are interested, you may want to give it a try.

System Dependencies

Before we start, we need a handy terminal emulation software. There are many such programs, but make sure to choose one that supports utf-8 encoding and 24-bit true color. Here I recommend.

There are also cross-platform terminal emulation software, such as Alacritty, which have some problems on different platforms, so beginners should not bother with them.

Everything in this article relies on a UNIX environment, windows users should install Windows Subsystem for Linux. This distribution has new software versions and is especially suitable for developers.

To lower the threshold of command line operation, I recommend using On My Zsh. Oh My Zsh provides mainly detailed command completion functions, which are very convenient.

Other software that needs to be installed are

  • git version control
  • tmux terminal reuse
  • fzf fuzzy matching for file travel
  • ripgrep file content search, much faster than grep
  • go language compiler

Finally, NeoVim is installed.

Basic Configuration

NeoVim’s default configuration file is ~/.config/nvim/init.vim.

Vim is compatible with vi by default, which is difficult to use, but NeoVim gives up compatibility with vi and enables many configuration items. So there are not many configurations that need to be changed.

I personally recommend adding the following configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
" 打开 24 位真彩色支持
set termguicolors
" 搜索的时候忽略大小字字母
set ignorecase
" 若搜索内容中有大写字母,则不再忽略大小写
set smartcase
" 高亮第80列
set colorcolumn=80
" 高亮光标所在行
set cursorline

This is all you need for the basic configuration. vim opens the file with the cursor on the first line by default. If you want to automatically jump to the last exit position, you need to add the following configuration.

1
2
3
4
autocmd BufReadPost *
      \ if line("'\"") >= 1 && line("'\"") <= line("$")
      \ |   exe "normal! g`\""
      \ | endif

It would take quite a bit of talking to make sense of how this configuration works. Let’s not get hung up on it. Let’s start by installing a few plugins.

Plugin Management

Vim has a lot of plugins for managing plugins. But I don’t recommend them, because you can easily manage them with git.

First, let’s go to the ~/.config/nvim directory and run git init.

Then we create the plugins directory by running mkdir -p pack/vendor/start. The name of the vendor directory can be whatever you want, but the pack/*/start three-level directory structure cannot be changed, it is specified by vim.

All plugins just need to be placed in the pack/*/start/ directory and they will work.

If we want to add a plugin we can.

1
git submodule add https://github.com/epii1/fzf.vim pack/vendor/start/fzf

If you want to update the plugin you can.

1
git submodule update --remote pack/vendor/start/fzf

If you want to remove the plugin you can.

1
git rm -rf pack/vendor/start/fzf

To be clear, this is a git submodule operation. Here we install the required plugins.

Theme plugins

First of all, we need to install a theme that looks a little bit better. I especially recommend tender, which is a theme that supports 24-bit true color and has a cooler tone.

1
git submodule add https://github.com/jacoborus/tender.vim pack/vendor/start/tender

Then you need to add the following configuration to open the theme

1
2
3
4
5
6
color tender
" 此处对 tender 主题略做调整,大家可以去掉对比一下效果
autocmd ColorScheme tender
\ | hi Normal guibg=#000000
\ | hi SignColumn guibg=#000000 "
\ | hi StatusLine guibg=#444444 guifg=#b3deef

File plug-in

Viewing the directory structure

The IDE interface displays a file manager on the left side, and Vim can achieve a similar effect with plugins. I recommend using the venerable NERDTree plugin.

1
git submodule add https://github.com/scrooloose/nerdtree pack/vendor/start/nerdtree

Once installed, you can open the file manager with :NERDTreeToggle. If you want NERDTree to locate the current file, you can execute the :NERDTreeFind command.

NERDTree supports many configurations, which can be viewed via :h NERDTree. It is recommended to add the following configuration.

1
2
3
let g:NERDTreeMinimalUI = 1
let g:NERDTreeChDirMode = 2
let g:NERDTreeWinSize = 24

Considering that NERDTree is more commonly used, we can set two shortcut keys

1
2
nnoremap <leader>e :NERDTreeToggle<cr>
nnoremap <leader>f :NERDTreeFind<cr>

nnoremap indicates that the shortcut key is set in Normal mode. <leader> defaults to \, and generally user-defined shortcut keys start with <leader> to prevent them from affecting the default key position. The final <cr> indicates a carriage return. So when we press \e in Normal mode, Vim will automatically simulate typing :NERDTreeToggle for us and then pressing enter. This is no different than typing the command and executing it ourselves.

NERDTree itself also supports writing extensions. The only extension I recommend here is nerdtree-git-plugin, which displays the status of git changes in NERDTree.

1
git submodule add https://github.com/Xuyuanp/nerdtree-git-plugin pack/vendor/start/nerdtree-git

nerdtree-git-plugin requires no additional configuration.

Search for file paths

If you’re not familiar with the project structure, start with NERDTree, but if you are, using NERDTree is a bit unwieldy. This is the time to use fzf as a big tool. I wrote a fzf.vim myself, which is pure and recommended for everyone.

1
git submodule add https://github.com/epii1/fzf.vim pack/vendor/start/fzf

fzf.vim needs a shortcut key of its own. I suggest using ctrl-p.

1
nnoremap <c-p> :call fzf#Open()<cr>

Mapping the ctrl key combination requires the use of pointed brackets, c for ctrl, otherwise it is no different from normal mapping.

This way, pressing ctrl-p in Normal mode opens a search window. Then you can keep typing characters to filter the search results, and finally press enter to open the corresponding file.

Search file contents

fzf.vim can only search file paths. If you want to search the contents of a file, you need to use the ripgrep command. I have also written my own plugin called ag.vim.

1
git submodule add https://github.com/epii1/ag.vim pack/vendor/start/ag

ag.vim uses the ag command by default. If you use ripgrep, you need to add the following line to configure it.

1
let g:ag_cli = 'rg'

If we want to search for the keyword fzf, then we can execute :Ag fzf. Regular expressions are supported, such as :Ag a[0-9]+ .

Recently used files

The last requirement is to record a list of recently opened files. I wrote a plugin mru.vim for this as well.

1
git submodule add https://github.com/epii1/mru.vim pack/vendor/start/mru

Again, you need to set a shortcut key, I use ctrl-u :)

1
nnoremap <c-u> :Mru<cr>

Once installed, mru.vim will automatically keep a list of recently used files, and will sort and de-duplicate them according to when they were used. Just press ctrl-u to open the file list. The cursor will stop at the last used file. You can open the last used file by pressing enter. You can also move the cursor and select other files.

The above are the main plug-ins related to file operations, which basically cover the common file operations.

Git

For beginners, you should in principle use the command line to work with git. But I found a plugin that shows the status of file changes in real time that I must recommend, called gitsigns.

gitsigns can display the change status of a file in real time on the far left side of the window, very fast. gitsigns is developed in lua and can only be run under NeoVim.

1
2
git submodule add https://github.com/nvim-lua/plenary.nvim pack/vendor/start/plenary
git submodule add https://github.com/lewis6991/gitsigns.nvim pack/vendor/start/gitsigns

Because it is written in lua, the way it is loaded and configured is very different from traditional plugins. We start by creating a vim.lua file in the directory where init.vim is located. This file can be named whatever you want, but not init.lua, and then we add a command to init.vim that loads the lua configuration

1
runtime vim.lua

The configuration of the lua plugin is written to the vim.lua file. gitsigns also requires little additional configuration, just add a line that says

1
require'gitsigns'.setup()

Note that the require syntax of lua is used here. You can import files without spaces, and it’s an expression with a return value, so you can call the setup() method directly on the return value.

There are many git-related plugins, but they are not recommended for beginners. You should wait until you are familiar with the git command before tinkering with it.

Syntax highlighting

Vim implements syntax highlighting by default using keywords and regular expressions. But this approach is slow and not smart enough.

NeoVim supports syntax highlighting and code folding using TreeSitter. TreeSitter does syntax analysis of the code, and incrementally, allowing for more fine-grained syntax coloring.

Installation is also very simple.

1
git submodule add https://github.com/nvim-treesitter/nvim-treesitter pack/vendor/start/treesitter

After restarting NeoVim, run :TSInstall to install the required languages.

1
:TSInstall go lua vim

If you need to update, then you can execute :TSUpdate.

Then we add the following code to vim.lua.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
require'nvim-treesitter.configs'.setup{
    -- 启用高亮
    highlight = {
        enable = true,
	-- 禁用 vim 基于正则达式的语法高亮,太慢
        additional_vim_regex_highlighting = false,
    },
    -- 启用缩进
    indent = {
        enable = true,
    },
}

The syntax here is rather odd, but common. In lua, both arrays and dictionaries are represented by {}. Arrays are {1,2} and dictionaries are {a=1,b=2}, which can be defined in nested sets. And lua allows parentheses to be omitted when calling functions. That means setup({a=1}) can be abbreviated to setup{a=1} and print("abc") can be abbreviated to print "abc".

We also need to make vim use treesitter to do code folding by adding the following configuration to init.vim.

1
2
3
4
5
6
" 使用 foldexpr 指定的方式折叠代码
set foldmethod=expr
" 使用 treesitter 根据语言语法折叠代码
set foldexpr=nvim_treesitter#foldexpr()
" 默认从第一级开始,大家可以去掉看有什么效果
set foldlevel=1

Then restart NeoVim and you’ll see the highlighting and collapsing effects.

Complementary jumps

The most important thing about writing code is the auto-completion and smart jump features. This is a bit tricky to configure.

In the old days, every IDE would implement a set of code completion and jumping features. Later, Microsoft released the language server protocol (lsp), which aims to abstract out the code completion and jumping features and provide the services to the public using a standard interface.

It was first used in vs code, but now more and more tools are starting to support lsp. NeoVim has a built-in lsp function.

The Go language also maintains its own official language server, called gopls, which we install first:

1
go install golang.org/x/tools/gopls@latest

Because configuring lsp is relatively cumbersome, each language needs to be configured separately. To make it easier to use, NeoVim maintains an official configuration collection for each language, which is distributed as a plugin. So we also need to install this configuration.

1
git submodule add https://github.com/neovim/nvim-lspconfig pack/vendor/start/lspconfig

Finally, you need to enable complement and jump support for the Go language by adding the following code to vim.lua.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
local on_attach = function(client, bufnr)
    -- 为方便使用,定义了两个工具函数
    local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
    local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end

    -- 配置标准补全快捷键
    -- 在插入模式可以按 <c-x><c-o> 触发补全
    buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

    local opts = { noremap=true, silent=true }

    -- 设置 normal 模式下的快捷键
    -- 第一个参数 n 表示 normal 模式
    -- 第二个参数表示按键
    buf_set_keymap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>', opts)
    -- 跳转到定义或者声明的地方
    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', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>', opts)
    -- 查看所有引用当前对象的地方
    buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>', opts)
    -- 跳转到下一个/上一个语法错误
    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)
    buf_set_keymap('n', '<c-k>', '<cmd>lua vim.lsp.buf.signature_help()<cr>', opts)
    -- 手工触发格式化
    buf_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<cr>', opts)
    buf_set_keymap('n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<cr>', opts)
    buf_set_keymap('n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<cr>', opts)
    buf_set_keymap('n', '<space>e', '<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<cr>', opts)
    -- 列出所有语法错误列表
    buf_set_keymap('n', '<space>q', '<cmd>lua vim.lsp.diagnostic.set_loclist()<cr>', opts)
    -- 修改当前符号的名字
    buf_set_keymap('n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<cr>', opts)
    buf_set_keymap('n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<cr>', opts)
    buf_set_keymap('n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<cr>', opts)
    buf_set_keymap('n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<cr>', opts)
end

require'lspconfig'.gopls.setup {
    on_attach = on_attach,
    capabilities = capabilities,
    flags = {
        debounce_text_changes = 150,
    },
}

Note that gopls is only supported for projects with go.mod added. Open a Go file, run :LspInfo, and you should see Configured servers list: gopls.

Find a function at random, press gd and it should jump to the corresponding function definition, return to it and press ctrl-t. NeoVim will start the gopls program the first time you use it, so there will be a slight delay. Subsequent use will be quick.

Switch to insert mode, type fmt. and press ctrl-x ctrl-o and you should see a pop-up completion window.

For other functions, please try it yourself according to the comments in the configuration.

This solution is by far the cleanest and fastest solution. However, there are two minor flaws in this solution.

  • Can’t automatically import the newly referenced package name
  • No automatic code formatting

How can I put up with this? After a bit of tinkering, we found a solution.

We need to create a ftplugin folder in the init.vim directory, and then create a new go.vim file in it, with the following contents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

" 这里用到在 vim 中嵌入 lua 代码的特殊语法
lua <<EOF
function org_imports(wait_ms)
    local params = vim.lsp.util.make_range_params()
    params.context = {only = {"source.organizeImports"}}
    local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, wait_ms)
    for _, res in pairs(result or {}) do
      for _, r in pairs(res.result or {}) do
        if r.edit then
          vim.lsp.util.apply_workspace_edit(r.edit)
        else
          vim.lsp.buf.execute_command(r.command)
        end
      end
    end
end
EOF

" 代码补全结束后自动导包
autocmd CompleteDone *.go :lua org_imports()
" 保存代码之前自动格式化
autocmd BufWritePre *.go :lua vim.lsp.buf.formatting()

Vim automatically executes the vim scripts in the plugin directory. This is not the case for the ftplugin directory. The ft prefix in the name is a shortened version of the file type, and the files in it are named with their extensions. For example, go.vim is only executed when the go source file is opened.

With the above configuration, NeoVim will automatically format the code and import the used package names when it exits.

Auto-completion

After the previous completion plugin is configured, you need to press ctrl-x ctrl-o in insert mode to trigger the completion. It is not as convenient as IDE. Can you support auto-trigger? Yes, but you need to install a new plugin. Here I recommend using cmp, also developed by lua, which is very fast.

You need to install six related plugins in one go.

1
2
3
4
5
6
git submodule add https://github.com/hrsh7th/nvim-cmp pack/vendor/start/cmp
git submodule add https://github.com/hrsh7th/cmp-path pack/vendor/start/cmp-path
git submodule add https://github.com/hrsh7th/cmp-vsnip pack/vendor/start/cmp-vsnip
git submodule add https://github.com/hrsh7th/cmp-buffer pack/vendor/start/cmp-buffer
git submodule add https://github.com/hrsh7th/cmp-nvim-lsp pack/vendor/start/cmp-lsp
git submodule add https://github.com/hrsh7th/vim-vsnip pack/vendor/start/vim-vsnip

Then add the following code to vim.lua.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local cmp = require'cmp'
cmp.setup {
    -- 必须指定 snippet 组件
    snippet = {
        expand = function(args) vim.fn['vsnip#anonymous'](args.body) end,
    },
    -- 配置补全内容来源
    sources = cmp.config.sources {
        -- 支持从打开的文件中补全内容
        { name = 'buffer', opts = { get_bufnrs = vim.api.nvim_list_bufs } },
	-- 支持从 lsp 服务补全
        { name = 'nvim_lsp' },
	-- 支持补全文件路径,可以输入 / 或者 ~ 体验
        { name = 'path' },
    },
}

-- 将 cmp-lsp 跟 lsp 服务关联起来
-- 需要更新一上之前的 gopls 配置
local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = require'cmp_nvim_lsp'.update_capabilities(capabilities)
require'lspconfig'.gopls.setup {
    on_attach = on_attach,
    flags = {
        debounce_text_changes = 150,
    },
}

So far you have done all the configuration work, enjoy it as much as you can.

One thing to note, however, is that the first time you use the lsp-related features, NeoVim will start gopls, and there will be a delay of a few seconds. After that, it will be very fast.

Summary

This article provides Go developers with a complete configuration manual, which implements most of the IDE features with minimal cost. Based on the configuration in this article, it should be easy to do your daily Go development work under NeoVim. I myself believe in the KISS principle, use only the necessary plugins and add only the necessary configuration. After you get started, you should focus on the basics of Vim, and then toss around plugins and configurations when you become proficient!