Merpay Architect / Mercari Microservices Platform チームの伊藤です。この記事は Merpay Tech Openness Month の3日目の投稿となります。本稿では、先日開催した開発ライブ実況のイベントで紹介した筆者の開発環境(Vim / Go)について、言語に依存しない「全般的な設定」と「Goの設定」の2つに大別して解説します。Vim に関する話題が多いですが、Go のために自作したツールについての解説はエディタに依存しないので、他のエディタを利用している方々もぜひご一読ください。
開発ライブ実況とは
「他人の開発風景を覗いてみよう!」というコンセプトのもとに弊社が開催しているコーディング実況イベントです。このイベントは、開発者が事前に課題(TODOアプリのサーバサイド等)に対するコーディングを録画し、その動画を他の開発者と見ながら実況放送する、というものです。
このイベントの第1回目で、筆者が Vim(Neovim)を使って Go でサーバを開発する風景を実況しました。実況動画のアーカイブはこちらになります。
全般的な設定
Fuzzy Finder
Fuzzy Finder としては fzf と、その Vim プラグインである fzf.vim を利用しています。 fzf
はエディタに依らない汎用的な CLI の Fuzzy Finder です。下図は fzf
と Rust で実装された高速な grep ツールである ripgrep を連携して、プロジェクトのファイル全体から特定の文字列を検索し、そのファイルにジャンプする例です。
上図の操作は下記のような設定を用いて行っています。
nnoremap <silent> <Leader>g :<C-u>silent call <SID>find_rip_grep()<CR>
function! s:find_rip_grep() abort
call fzf#vim#grep(
\ 'rg --ignore-file ~/.ignore --column --line-number --no-heading --hidden --smart-case .+',
\ 1,
\ fzf#vim#with_preview({'options': '--delimiter : --nth 4..'}, 'right:50%', '?'),
\ 0,
\ )
endfunction
fzf.vim
がデフォルトで提供しているファイル一覧や grep 以外にも、ブランチ一覧やプロジェクト一覧、シンボルの絞り込みなど、何かを絞り込んで検索したいときには全て fzf
を経由するように設定しています。
Language Server Protocol クライアント
Language Server Protocol (LSP) は、補完候補や定義箇所といったソースコードに対する情報を提供するツール(Language Server)と、それを用いるテキストエディタやIDEとのやりとりを標準化したプロトコルです。このプロトコルが普及する以前では、「各言語毎に専用に作られたプラグインをエディタに導入する」というのが一般的でしたが、現在では「エディタには単一のLSPクライントを導入し、LSP 経由で各言語の Language Server と連携する」というのが一般的になりつつあります。
筆者は LSP 経由で補完候補の表示、定義箇所へのジャンプ、シンボルの一覧表示、シンボルのリネーム、コンパイルエラーや Linter の違反情報の表示などを行っています。LSP クライアントとしての Vim プラグインはいくつか存在しますが、下記の3つが代表的です。
筆者はこの3つのプラグインを全て導入して定期的に試していますが、fzf との連携が組み込まれているという観点から、現在は LanguageClient-neovim
を主に利用しています。下記は筆者が行っている LanguageClient-neovim
の設定の一部です(設定の内容についてはLanguageClient-neovimのドキュメントを参照してください)。
" 各言語の Language Server の起動コマンド
let g:LanguageClient_serverCommands = {
...
\ 'go': [
\ '/path/to/gopls',
\ '-remote=:37374',
\ '-remote.logfile=/tmp/gopls.remote.log',
\ ]
\ 'rust': [
\ '/path/to/rls',
\ ],
...
\ }
let g:LanguageClient_diagnosticsSignsMax = v:null
let g:LanguageClient_changeThrottle = v:null
let g:LanguageClient_autoStart = 1
let g:LanguageClient_autoStop = 1
let g:LanguageClient_selectionUI = 'fzf'
let g:LanguageClient_selectionUI_autoOpen = 0
let g:LanguageClient_trace = 'off'
let g:LanguageClient_diagnosticsEnable = 1
let g:LanguageClient_loadSettings = 1
let g:LanguageClient_windowLogMessageLevel = 'Warning'
let g:LanguageClient_hoverPreview = 'Always'
let g:LanguageClient_fzfContextMenu = 1
let g:LanguageClient_diagnosticsList = 'Quickfix'
let g:LanguageClient_useVirtualText = 'Diagnostics'
let g:LanguageClient_virtualTextPrefix = '>> '
let g:LanguageClient_useFloatingHover = 1
let g:LanguageClient_usePopupHover = 1
let g:LanguageClient_floatingHoverHighlight = 'Normal:Pmenu'
let g:LanguageClient_completionPreferTextEdit = 0
let g:LanguageClient_waitOutputTimeout = 100
let g:LanguageClient_echoProjectRoot = 0
let g:LanguageClient_hideVirtualTextsOnInsert = 1
let g:LanguageClient_diagnosticsMaxSeverity = 'Hint'
let g:LanguageClient_applyCompletionAdditionalTextEdits = 1
let g:LanguageClient_preferredMarkupKind = ['markdown', 'plaintext']
let g:LanguageClient_floatingWindowStyle = 'minimal'
let g:LanguageClient_diagnosticsIgnoreSources = ['go mod tidy']
" ドキュメントのハイライトをトグルさせるための設定
let s:ls_is_highlighting_document = v:false
function! s:ls_toggle_document_highlight() abort
if !s:ls_is_highlighting_document
call LanguageClient#textDocument_documentHighlight()
let s:ls_is_highlighting_document = v:true
else
call LanguageClient#clearDocumentHighlight()
let s:ls_is_highlighting_document = v:false
end
endfunction
" ドキュメントの表示をトグルさせるための設定
function! s:ls_toggle_document_hover() abort
let hover_buf = s:ls_get_hover_buf()
if hover_buf == 0
call LanguageClient#textDocument_hover()
else
execute(printf('bwipe! %s', hover_buf))
endif
endfunction
function! s:ls_get_hover_buf() abort
for w in nvim_list_wins()
let bufnum = nvim_win_get_buf(w)
let bufname = bufname(bufnum)
if bufname ==# '__LanguageClient__'
return bufnum
endif
endfor
return 0
endfunction
" 定義ジャンプ後にコールバックで呼ぶ関数
function! Language_client_definition_callback(output, ...) abort
normal! zz
call vista#util#Blink(3, 100)
endfunction
" マッピングの設定
function! s:map_language_client_functions() abort
if has_key(g:LanguageClient_serverCommands, &filetype)
nnoremap <silent> gv :<C-u>call LanguageClient#explainErrorAtPoint()<CR>
nnoremap <silent> gi :<C-u>call LanguageClient#textDocument_implementation()<CR>
nnoremap <silent> gkr :<C-u>call LanguageClient#textDocument_references()<CR>
nnoremap <silent> gr :<C-u>call LanguageClient#textDocument_rename()<CR>
nnoremap <silent> <Leader>i :<C-u>call <SID>ls_toggle_document_hover()<CR>
nnoremap <silent> gd :<C-u>call LanguageClient#textDocument_definition({'handle': v:true}, function('Language_client_definition_callback'))<CR>
nnoremap <silent> gsi :<C-u>silent call <SID>ls_toggle_document_highlight()<CR>
nnoremap <silent> gc :<C-u>silent Vista finder fzf:lcn<CR>
endif
endfunction
autocmd vimrc FileType * call s:map_language_client_functions()
LanguageClient-neovim
は補完のためのプラグインである deoplete.nvim と連携する機能を内包しており、 deoplete.nvim
が導入されていると自動的に deoplete.nvim
経由で補完候補が表示されます。下図は LanguageClient-neovim
と deoplete.nvim
を連携させて補完を行っている例です。
関数のシグネチャは echodoc.vim というプラグインを用いてカーソルの上の別ウィンドウ(Floating Window)に表示しています。
Git
筆者は tig を利用して Git の各操作を行っています。tig
は CLI で動作する Git クライアントであり、 git add
などの操作を CLI でインタラクティブに行うためのツールです。この tig
を Neovim のターミナル機能を用いて Vim 上で起動しています。下図は Vim 上で tig
を起動し、対象のファイルを git add
した後に git commit
する例です。
上図で、Vim 上のターミナルから起動した tig
で git commit
を行うときに、新たな Vim のプロセスを生成するのではなく、親側の Vim プロセスでコミットの編集画面が開く点に注目してください。これは neovim-remote という Neovim をリモートで操作するためのツールを用いて実現しています。
これを踏まえて、上図の操作は下記のような設定を用いて実行されています。
" neovim-remote
let nvrcmd = "nvr --remote-wait -cc 'call NvrBeforeCmd()' -c 'call NvrAfterCmd()'"
let $VISUAL = nvrcmd
let $GIT_EDITOR = nvrcmd
nnoremap <silent> <Leader>t :<C-u>silent call <SID>tig_status()<CR>
function! s:tig_status() abort
call s:open_term('tig status')
endfunction
function! s:open_term(cmd) abort
let split = s:split_type()
call execute(printf('%s term://%s', split, a:cmd))
setlocal bufhidden=delete
setlocal noswapfile
setlocal nobuflisted
endfunction
function! s:split_type() abort
" NOTE: my cell ratio: width:height == 1:2.1
let width = winwidth(win_getid())
let height = winheight(win_getid()) * 2.1
if height > width
return 'split'
else
return 'vsplit'
endif
endfunction
Vim の起動時に VISUAL
と GIT_EDITOR
環境変数を nvr
(neovim-remote が提供するコマンド)に置き換えることで、Vim 上から起動した tig
でコミットを行うときは nvr --remote-wait ...
を経由して親側の Neovim プロセスでコミット編集画面が開くように調整しています。 s:open_term
関数はターミナルでプログラムを開くときのバッファの設定を調整するための関数です。s:split_type
関数は新規ウィンドウを縦・横どちらの分割で開くかを決定するためのもので、これを用いて「現在のウィンドウが縦長なときは横分割、横長なときは縦分割」で新規ウィンドウを開くようにしています。
ファイラ
ファイルの作成や削除、コピー&ペーストは defx.nvim というファイラプラグインを用いて下図のように Vim 上から実行できるようにしています。
上図の操作は下記のような defx.nvim
の設定を行うことで実現しています。
autocmd vimrc FileType defx call s:map_defx_functions()
autocmd vimrc FileType defx call defx#custom#column('mark', {
\ 'length': 1,
\ 'readonly_icon': 'X',
\ 'selected_icon': '*',
\ })
autocmd vimrc FileType defx call defx#custom#column('indent', {
\ 'indent': ' ',
\ })
autocmd vimrc FileType defx call defx#custom#column('icon', {
\ 'directory_icon': '▸',
\ 'opened_icon': '▾',
\ 'root_icon': ' ',
\ })
autocmd vimrc BufLeave,BufWinLeave \[defx\]* silent call defx#call_action('add_session')
function! s:map_defx_functions() abort
nnoremap <silent><buffer><expr> <CR> <SID>defx_edit()
nnoremap <silent><buffer><expr> c defx#do_action('copy')
nnoremap <silent><buffer><expr> m defx#do_action('move')
nnoremap <silent><buffer><expr> p defx#do_action('paste')
nnoremap <silent><buffer><expr> l defx#do_action('open')
nnoremap <silent><buffer><expr> o defx#do_action('open_or_close_tree')
nnoremap <silent><buffer><expr> O defx#do_action('open_tree_recursive')
nnoremap <silent><buffer><expr> E defx#do_action('open', 'vsplit')
nnoremap <silent><buffer><expr> P defx#do_action('open', 'pedit')
nnoremap <silent><buffer><expr> K defx#do_action('new_directory')
nnoremap <silent><buffer><expr> N defx#do_action('new_file')
nnoremap <silent><buffer><expr> d defx#do_action('remove')
nnoremap <silent><buffer><expr> r defx#do_action('rename')
nnoremap <silent><buffer><expr> x defx#do_action('execute_system')
nnoremap <silent><buffer><expr> yy defx#do_action('yank_path')
nnoremap <silent><buffer><expr> . defx#do_action('toggle_ignored_files')
nnoremap <silent><buffer><expr> h defx#do_action('cd', ['..'])
nnoremap <silent><buffer><expr> ~ defx#do_action('cd')
nnoremap <silent><buffer><expr> q defx#do_action('quit')
nnoremap <silent><buffer><expr> <Leader>q defx#do_action('quit')
nnoremap <silent><buffer><expr> <Space> defx#do_action('toggle_select') . 'j'
nnoremap <silent><buffer><expr> * defx#do_action('toggle_select_all')
nnoremap <silent><buffer><expr> <C-l> defx#do_action('redraw')
nnoremap <silent><buffer><expr> <C-g> defx#do_action('print')
nnoremap <silent><buffer><expr> cd defx#do_action('change_vim_cwd')
nnoremap <silent><buffer><expr> j line('.') == line('$') ? 'gg' : 'j'
nnoremap <silent><buffer><expr> k line('.') == 1 ? 'G' : 'k'
nnoremap <silent><buffer> D :<C-u>call <SID>defx_reload()<CR>
endfunction
nnoremap <silent> <Leader>e :<C-u>silent call <SID>open_or_close_defx()<CR>
function! s:defx_edit(...) abort
let args = defx#util#convert2list(get(a:000, 0, []))
let cmd = printf(":\<C-u>call DefxEditFile(%s)\<CR>", string(args))
return cmd
endfunction
function! DefxEditFile(args, ...) abort
" ...
call defx#call_action('open', a:args)
endfunction
function! s:open_or_close_defx() abort
if &filetype ==# 'defx'
bwipe
else
call s:open_defx()
end
endfunction
function! s:open_defx() abort
let opts = [
\ '-no-show-ignored-files',
\ '-ignored-files=.git,.nvimrc,.lc.*,_tmp',
\ '-sort=filename',
\ '-no-listed',
\ '-no-new',
\ '-buffer-name=defx',
\ '-split=no',
\ printf('-session-file=%s', '.defx_session.json'),
\ ]
call defx#util#call_defx('Defx', join(opts, ' '))
if len(filter(getbufinfo({'buflisted': 1}), 'v:val.name !=# ""')) == 0
call s:wipe_all_listed_buffers()
endif
endfunction
function! s:defx_reload() abort
execute 'normal q'
call s:defx_delete_session()
call s:open_defx()
endfunction
function! s:defx_delete_session() abort
let jq_expression = printf(
\ '.sessions | map_values(select(.path != "%s")) | {version: "1.0", sessions: .}',
\ getcwd(),
\ )
let jq_cmd = printf("jq '%s'", jq_expression)
let cmd = printf('J=$(cat %s | %s) && echo $J > %s', '.defx_session.json', jq_cmd, '.defx_session.json')
call system(cmd)
endfunction
スニペット
タイプ数を極力減らすために筆者はコードスニペットを多用しています。スニペット用のプラグインとしては UltiSnips を利用しています。 UltiSnips
は下記のように設定しています。
let g:UltiSnipsUsePythonVersion = 3
let g:UltiSnipsEditSplit = 'normal'
let g:UltiSnipsSnippetDirectories = ['/path/to/snippets_dir']
let g:UltiSnipsEnableSnipMate = 0
let g:UltiSnipsExpandTrigger = '<c-k>'
let g:UltiSnipsJumpForwardTrigger = '<c-f>'
let g:UltiSnipsJumpBackwardTrigger = '<c-b>'
command! SNIP UltiSnipsEdit
設定にある通り、基本的にはスニペットを入力した後に ctrl + k
を入力することでスニペットを展開していますが、 UltiSnips
の自動展開機能を用いて、特定の文字列が入力されたときはトリガー(ctrl + k
)の入力なしで自動でスニペットを展開するように設定しています。下図はその例です。
上図では、 cbac
という入力が行われたときにスニペットのトリガーである ctrl + k
の入力を行わずに context.Background()
が展開されています。このスニペットは下記のような設定になっています。
snippet cbac "context.Background" wA
context.Background()
endsnippet
UltiSnips
ではスニペットに A
オプションを渡すことでトリガーを入力せずにスニペットを自動展開させることができます。
UltiSnips
はこの他にも Python や Vim Script を用いてスニペットの展開内容をダイナミックに生成する機能などを持っており、これを用いることで柔軟にスニペットを生成することが可能となります。
Goの設定
gopls
Go の Language Server としては公式に提供されている gopls
を利用しています。gopls
はエディタを起動したときに gopls
自体も同時に起動するのが一般的ですが、この方法だと gopls
の起動を待つ時間が発生しやすくなります。これをできるだけ回避するために、筆者は gopls
を daemon mode で起動しています。daemon mode
で gopls
を起動すると gopls
は daemon プロセスとして常に動き続け、エディタは起動時にその daemon プロセスに接続する形となります。これにより gopls
の起動待ちの時間を短縮することができます。筆者は主に Linux(Ubuntu)で開発しているので、下記のような設定で systemd
を用いて gopls
プロセスを永続化しています。
[Unit]
Description=Gopls Remote Instance
[Service]
Type=simple
ExecStart=/bin/bash -c 'PATH=$HOME/go/bin:$PATH /path/to/gopls -listen=:37374 -logfile=/tmp/gopls.daemon.log'
Restart=always
[Install]
WantedBy=default.target
テストの実行
実装中にテストを頻繁に実行するので、下図のようにカーソル上のテスト関数を go test -run
で Vim 上から実行できるように設定しています。
これを行うために、自作した go-test-name というツールを用いて下記のような設定を行っています。
autocmd vimrc FileType go nnoremap <silent> gt :<C-u>silent call <SID>go_test_function()<CR>
function! s:go_test_function() abort
let test_info = json_decode(system(printf('go-test-name -pos %s -file %s', s:cursor_byte_offset(), @%)))
for b in nvim_list_bufs()
if bufname(b) ==# 'vim-go-test-func'
execute printf('bwipe! %s', b)
endif
endfor
let dir = expand('%:p:h')
if len(test_info.sub_test_names) > 0
let cmd = printf("go test -coverprofile='/tmp/go-coverage.out' -count=1 -v -race -run='^%s$'/'^%s$' $(go list %s)", test_info.test_func_name, test_info.sub_test_names[0], dir)
else
let cmd = printf("go test -coverprofile='/tmp/go-coverage.out' -count=1 -v -race -run='^%s$' $(go list %s)", test_info.test_func_name, dir)
endif
let split = s:split_type()
execut printf('%s gotest', split)
if split ==# 'split'
execute(printf('resize %s', floor(&lines * 0.3)))
endif
call termopen(cmd)
setlocal bufhidden=delete
setlocal noswapfile
setlocal nobuflisted
file vim-go-test-func
wincmd p
endfunction
function! s:cursor_byte_offset() abort
return line2byte(line('.')) + (col('.') - 2)
endfunction
go-test-name
はソースコードを静的に解析し、カーソル(バイトオフセット)上のテスト関数名とサブテスト名をJSONで返すツールです。このツールが返す情報を、 go test
コマンドの -run
引数に渡し、Vim 上のターミナルから実行することにより、単体のテスト、及びサブテストのみをエディタから即座に実行できるように設定しています。
デバッグの実行
筆者はテストの実行をデバッガ経由で実行することで、プログラムのデバッグを行っています。Go で書いたプログラムのデバッグには delve を用いてます。テストと同様にカーソル上のテスト関数を dlv test
で Vim 上から実行できるように設定しています。
上図は、テスト関数内にブレークポイントを設定した後にデバッガを実行し、変数の中身を print して動作の確認を行っている例です。設定は下記のようになっています。
command! -nargs=* -bang GoDebugTestFunc call s:go_debug_test_function(<bang>0, 0, <f-args>)
autocmd vimrc FileType go nnoremap <silent> gb :<C-u>GoDebugTestFunc<CR>
autocmd vimrc Filetype go command! -nargs=* -bang BP call s:go_debug_toggle_break_point(<f-args>)
autocmd vimrc Filetype go command! -nargs=* -bang BPC call s:go_debug_clear_breakpoionts()
function! s:go_debug_test_function(bang, ...) abort
call writefile(g:godebug_breakpoints + ['continue'], g:godebug_breakpoints_file)
let test_info = json_decode(system(printf('go-test-name -pos %s -file %s', s:cursor_byte_offset(), @%)))
let test = search(printf('func %s', test_info.test_func_name), 'bcnW')
let tmpl = "cd %s && GOMAXPROCS=1 dlv test --init=%s -- -test.run='^%s$'"
let wd = expand('%:h')
let dlv = printf(tmpl, wd, g:godebug_breakpoints_file, test_info.test_func_name)
if len(test_info.sub_test_names) > 0
let dlv = printf("%s/'^%s$'", dlv, test_info.sub_test_names[0])
endif
enew
set syntax=go
call termopen(dlv)
file debug
start
endfunction
function! s:go_debug_toggle_break_point(...) abort
let bp_file = expand('%:p')
let line = line('.')
let breakpoint = printf('break %s:%s', bp_file, line)
exe 'sign define gobreakpoint text=>> texthl=EmphasisLightBlue'
let i = index(g:godebug_breakpoints, breakpoint)
if i == -1
call add(g:godebug_breakpoints, breakpoint)
execute printf('sign place %s line=%s name=gobreakpoint file=%s', line, line, bp_file)
else
call remove(g:godebug_breakpoints, i)
execute printf('sign unplace %s file=%s', line, bp_file)
endif
endfunction
function! s:go_debug_clear_breakpoionts() abort
for b in g:godebug_breakpoints
let point = matchlist(b, '\vbreak (.+):(\d+)')
let file = point[1]
execute printf('sign unplace %s file=%s', point[2], file)
endfor
let g:godebug_breakpoints = []
endfunction
function! s:go_debug_delete_break_points_file(...) abort
if filereadable(g:godebug_breakpoints_file)
call delete(g:godebug_breakpoints_file)
endif
endfunction
「テスト」の項で解説したのと同様に、 go-test-name
を用いてカーソル上のテスト関数名を特定し、そのテスト関数名を dlv test
コマンドの -test.run
引数に渡して Vim 上のターミナルで実行するように設定しています。(この設定は vim-godebug というプラグインを参考にしています。)
interface の実装
impl という Go の interface のスタブを生成するツールを用いて、カーソル上の struct に対して プロジェクト内の interface を実装させる設定を行っています。
上図の例は下記のような impl
連携の設定を用いて実現しています。
autocmd vimrc Filetype go command! IMP call s:go_fzf_implement_interface()
function! s:go_fzf_implement_interface() abort
let source = 'go_list_interfaces'
call fzf#run({
\ 'source': source,
\ 'sink': function('s:go_implement_interface'),
\ 'down': '40%'
\ })
endfunction
function! s:go_implement_interface(interface) abort
call s:go_execute_impl(a:interface, v:false)
endfunction
function! s:go_execute_impl(interface, is_std_pkg) abort
let pos = getpos('.')
let recv = s:go_receiver()
" Make sure we put the generated code *after* the struct.
if getline('.') =~# 'struct '
normal! $%
endif
if !a:is_std_pkg
let pkg = system('go mod edit -json | jq -r .Module.Path | tr -d "\n"')
if a:interface =~# '^\.'
let interface = printf('%s%s', pkg, a:interface)
else
let interface = printf('%s/%s', pkg, a:interface)
endif
else
let interface = a:interface
end
try
let dirname = fnameescape(expand('%:p:h'))
let [result, err] = go#util#Exec(['impl', '-dir', dirname, recv, interface])
let result = substitute(result, "\n*$", '', '')
if err
call go#util#EchoError(result)
return
endif
if result is# ''
return
end
put =''
silent put =result
finally
call setpos('.', pos)
endtry
endfunction
上記の設定の一部は Vim 用の Go プラグインである vim-go から拝借しています。上記の設定で呼び出している go_list_interfaces
コマンドは inteface の一覧を表示するためのもので、下記のようなシェルスクリプトになっています。
#!/bin/bash
rg '^type [A-Z].* interface' --type go --ignore-file ~/.ignore --no-heading --no-line-number | while read line; do
pkg=$(echo -e $line | perl -nle 'm/^(.*)\/(.+)\.go:/; print $1')
type=$(echo -e $line | perl -nle 'm/^.*type (.*) interface/; print $1')
echo "${pkg}.${type}"
done
無理やりシェルスクリプトで interface を絞り込んでいますが、本来は静的解析で行うべきものなので、knife などのツールを利用するように修正しようと考えています。
式の補完
今回のイベントのために録画した自分の開発動画を見返すことにより、筆者は「右辺を入力してから左辺を書く」ことが多いということに気づきました。そこから発想を得て新たに go-expr-completion というツールを開発しました。このツールはソースコードを静的解析して「カーソル上の式についての型情報」を返すツールです。これを用いてカーソル上の式に対する左辺を自動生成するための Vim プラグインが vim-go-expr-completion です。下図はこのプラグインを用いて左辺を自動生成している例です。
vim-go-expr-completion
は式に対する左辺と同時に、必要であればエラーチェックのための if err != nil
文も自動で生成します。
おわりに
本稿では、筆者が Vim(Neovim)の環境をどのように構築しているかについて解説しました。Go は静的解析用のライブラリを標準で提供しており、エディタ用のツールが比較的簡単に実装できるので、皆様もぜひ新しい便利ツールを作ってエディタを拡張してみてはいかがでしょうか。本稿で紹介した内容が皆様のエディタライフになにか寄与できれば光栄です。それでは。