如何配置 Vim 的 Golang 开发环境

2021-11-10 ⏳7.9分钟(3.1千字)

今天给大家介绍一下如何配置 Vim 下的 Go 开发环境,功能上不输 GoLand。有兴趣的同学不妨折腾一把。

系统依赖

在开始之前,我们需要一款趁手的终端模拟软件。此类软件有很多,但一定要选择支持 utf-8 编码以及 24 位真彩色的终端。在这里我推荐:

还有一些跨平台的终端模拟软件,比如Alacritty,它们在不同平台都多少会有一些问题,初学者就不要折腾了。

本文所有内容都依赖 UNIX 环境,windows 用户请安装 Windows Subsystem for Linux。我建议选择 ArchLinux 系统。该发行版的软件版本都很新,特别适合开发者使用。

为了降低命令行操作的门槛,我建议大家使用On My Zsh 。Oh My Zsh 主要提供了详细的命令补全功能,非常方便。

其他需要安装的软件有:

最后安装 NeoVim。为什么用 NeoVim 还不是传统的 Vim 可以参考我的文章

基础配置

NeoVim 的默认配置文件是~/.config/nvim/init.vim

Vim 默认兼容 vi,比较难用。NeoVim 则放弃兼容 vi,开启了很多配置项。所以需要改的配置不是很多。

我个人推荐添加以下配置:

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

基础配置也就需要这些了。Vim 默认打开文件的时候光标会停在第一行。如果想自动跳转到上次退出的位置,则需要添加以下配置:

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

要想讲清楚这段配置的工作过程,要费不少口舌。大家先不要纠结。我们先装几个插件。

插件管理

Vim 有很多管理插件的插件。但我并不推荐大家使用,因为使用 git 就能轻松管理插件。

首先,我们到~/.config/nvim目录,执行git init

然后创建插件目录,执行mkdir -p pack/vendor/start。vendor 目录的名字可以随便,但pack/*/start三级目录结构不能变,这是 vim 规定的。

所有的插件只需放到pack/*/start/目录下就能工作。

如果我们想添加一个插件可以:

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

如果想更新插件可以:

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

如果想删除插件可以:

git rm -rf pack/vendor/start/fzf

说白了就是 git 子模块操作。下面我们安装需要的插件。

主题插件

首先,我们需要安装一个稍微能看的主题。在此我特别推荐 tender。这是一种支持 24 位真彩色的主题,色调比较冷。

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

然后需要添加如下配置开启主题

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

文件插件

查看目录结构

IDE 界面的左边都会显示文件管理器,Vim 通过插件也能实现类似效果。我推荐使用老牌的 NERDTree 插件。

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

装好之后可以通过:NERDTreeToggle打开文件管理器。如果想让 NERDTree 定位到当前文件,则可以执行:NERDTreeFind命令。

NERDTree 支持很多配置,可以通过:h NERDTree查看。推荐添加如下配置:

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

考虑到 NERDTree 比较常用,我们可以设置两个快捷键

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

nnoremap 表示在 Normal 模式下设置快捷键。<leader>默认是\,一般用户定义的快捷键都以<leader>开始,防止影响默认键位。最后的<cr>表示回车。所以,当我们在 Normal 模式下按\e的时候,Vim 会为我们自动模拟输入:NERDTreeToggle,然后按回车。这跟我们自己输入命令并执行没有什么区别。

NERDTree 本身也支持写扩展。在这里我只推荐一款扩展:nerdtree-git-plugin,它可以在 NERDTree 中显示 git 变更状态。

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

nerdtree-git-plugin 不需额外配置。

搜索文件路径

如果对项目结构不熟悉,那首先 NERDTree。但如果已经非常熟悉了,使用 NERDTree 就略显笨重。这个时候就要祭出 fzf 这个大杀器了。我自己写了一个 fzf.vim,功能比较纯粹,推荐给大家。关于 fzf.vim 的工作原理可以参考这篇文章。而我为什么要开发自己的 fzf.vim 可以参考这篇文章

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

fzf.vim 需要自行添加一个快捷键。我建议使用ctrl-p

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

映射 ctrl 组合键需要使用尖括号,c 表示 ctrl,其他跟普通映射没有区别。

这样在 Normal 模式下按下ctrl-p,就会打开一个搜索窗口。然后可以不断输入字符来过滤搜索结果,最后按回车打开对应文件。

搜索文件内容

fzf.vim 只能搜索文件路径。如果想搜索文件的内容,则需要使用 ripgrep 命令。我同样自己写了一个插件,就叫 ag.vim。工作原理可以参考我的文章

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

ag.vim 默认使用 ag 命令。如果使用 ripgrep 需要加一行配置:

let g:ag_cli = 'rg'

如果我们想搜索关键词fzf,则可以执行:Ag fzf。支持正则表达式,比如:Ag a[0-9]+

最近使用文件

最后一个需求是记录最近打开过的文件列表。这个我同样写了一个插件 mru.vim。工作原理可以参考我的这篇文章

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

同样,需要设置一个快捷键,我用的是ctrl-u

nnoremap <c-u> :Mru<cr>

安装以后,mru.vim 会自动记录最近使用的文件列表,而且还会按照使用时间进行排序和去重。只要按下ctrl-u就可以打开文件列表。光标会停在上次使用的文件。直接按回车就可以打开上次使用的文件。也可以移动光标,选择其他文件。

以上就是文件操作相关的主要插件,基本涵盖了常用的文件操作。

Git

对于初学者,原则上应该直接使用命令行操作 git。但我发现一款可以实时展示文件修改状态的插件,必须推荐给大家,叫gitsigns

gitsigns 可以在窗口的最左边实时显示文件的变更状态,非常快。gitsigns 使用 lua 开发,只能在 NeoVim 下运行。

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

因为是 lua 编写,所以加载和配置方式跟传统的插件有很大的区别。我们先在 init.vim 所在目录创建 vim.lua 文件。这个文件名可以随便取,但不能叫 init.lua。然后我们在 init.vim 添加加载 lua 配置的指令:

runtime vim.lua

lua 插件的配置都写到 vim.lua 文件。gitsigns 也几乎不用额外配置,只需添加一行:

require'gitsigns'.setup()

注意了,这里用到了 lua 的require语法。导入文件的时候可以不加空格,而且这是一个表达式,有返回值,可以直接调用返回值的setup()方法。

git 相关的插件有很多,但不建议初学者使用。等 git 命令用得滚瓜烂熟了再折腾不迟。

语法高亮

Vim 默认使用关键词和正则表达式实现语法高亮。但这种方式比较慢,而且也不够智能。

NeoVim 支持使用TreeSitter进行语法高亮和代码折叠。TreeSitter 会对代码做语法分析,而且还是增量分析,可以实现更精细化的语法着色功能。

安装也非常简单:

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

重启 NeoVim 后执行:TSInstall安装需要的语言:

:TSInstall go lua vim

如果需要更新,则可以执行:TSUpdate

然后我们在 vim.lua 加添加如下代码:

require'nvim-treesitter.configs'.setup{
    -- 启用高亮
    highlight = {
        enable = true,
	-- 禁用 vim 基于正则达式的语法高亮,太慢
        additional_vim_regex_highlighting = false,
    },
    -- 启用缩进
    indent = {
        enable = true,
    },
}

这里的语法比较怪,但很常见。在 lua 中,数组和字典都用{}表示。数组为{1,2},字典为{a=1,b=2},可以嵌套定义。而 lua 在调用函数的时候允许略括号。也就是说setup({a=1})可以简写成setup{a=1}print("abc")可以简写成print "abc"

我们还需要让 vim 使用 treesitter 做代码折叠,在 init.vim 添加如下配置:

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

然后重启 NeoVim 就能看到高亮和折叠效果了。

补全跳转

写代码最重要的就是自动补全和智能跳转功能。这个配置起来比较麻烦。

在以前,每个 IDE 都会实现一套代码补全和跳转功能。后来,微软发布了 language server protocol(lsp),目的是把代码补全和跳转等功能抽象出来,使用标准接口对外提供服务。

最早应该是 vs code 中使用,现在起来越多的工具都开始支持 lsp 协议。其中 NeoVim 更是内置了 lsp 功能。

Go 语言官方也维护自己的 language server,叫 gopls。我们先把它装上:

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

因为配置 lsp 相对比较麻烦,每种语言都需要单独配置。为了方便使用,NeoVim 官方维护了种语言的配置合辑,以插件的形式对外发布。所以我们还需要安装这套配置:

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

最后,还需要为 Go 语言开启补全和跳转支持,需要在 vim.lua 加入如下代码:

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,
    },
}

完成上述配置,就可以重启 NeoVim。注意,gopls 仅仅支持添加了 go.mod 的项目。打开一个 Go 文件,执行:LspInfo命令,应该会看到Configured servers list: gopls

随便找一个函数,按gd应该就会跳转到对应的函数定义,返回按ctrl-t。第一次使用的时候 NeoVim 会启动 gopls 程序,所以会有一点延迟。后续使用就很快了。

切换到插入模式,输入fmt.然后按ctrl-x ctrl-o应该就会看到弹出的补全窗口。

其他功能请根据配置里的注释自行尝试。

这个方案是目前最简洁的方案,也是最快的方案。但这个方案有两个小瑕疵:

这怎么能忍!途经折腾,找到了解决方案。

我们需要在 init.vim 目录创建一个ftplugin文件夹,然后在里面新建 go.vim 文件,内容如下:

" 这里用到在 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 会自动执行plugin目录下的 vim 脚本。但ftplugin目录下的则不然。名字里的 ft 前缀是 file type 的简写,里面的文件都是以扩展名为命名的。比如 go.vim 就只在打开 go 源码文件的时候才执行。

有了上面的配置,NeoVim 在退出的时候就会自动格式化代码并导入使用的包名。

自动补全

前面的补全插件配好以后,需要在插入模式按ctrl-x ctrl-o才能触发补全。不如 IDE 方便。那能不能支持自动触发呢?能,但要安装新的插件。在这里我推荐使用 cmp,也是 lua 开发的,非常快。

需要一口气安装六个相关插件:

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

然后在 vim.lua 添加如下代码:

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,
    },
}

到现在就完成了所有的配置工作,尽量享用吧。

不过有一点需要注意,首次使用 lsp 相关功能的时候,NeoVim 会启动 gopls,会有一个几秒钟的延迟。启动之后就很快了。

总结

本文给 Go 开发者提供了一份比较完备的配置手册,用最小的成本实现了 IDE 大多数功能。基于本文的配置,应该可以很方便地在 NeoVim 下完成 Go 日常开发工作。我本身推崇 KISS 原则,只用必要的插件,只加必要的配置。大家在入门之后,还是要把精力放到 Vim 基础操作上,等熟练了再去折腾各种插件和配置。Vim 基础学习可以参考我写的系列文章