2024年版 Go + Docker + VSCode + air + delveの環境でサクサクローカル開発
こんにちは、kensho(@iwashi623)です。
皆さんはデバッガー、使ってますか?
文明の利器たるデバッガー、私はローカル開発をするときにどうしても使いたい派の人間です。
最近担当することになったアプリケーションのローカル環境でも、デバッガーを使いたかったのですが、あいにく未導入の状態でした。
そこで、Goのデバッガーであるdelveを導入して、ブレークポイントを貼って開発できる状態にしました。
構成は、今どきよくありそうな、
Docker
Golang(echo)
air
といった環境です。
delve
の導入時、先人達のZennやQiita、ブログの記事をみて色々試してみたのですが、どうにも僕の手元ではおかしな挙動をして困ったりしていました。
今回はその格闘のログと、これでいいかと落ち着いた設定を書いていきます。
環境
ネタバレで先に僕が最終的に落ち着いた設定を書いておきます。
GitHubで公開しています。
リポジトリ
- エディタ:
VSCode
- 実行環境:
Docker(Docker Compose)
- Golang: 1.23.0
- FW: echo v4
- ホットリロードをしたいので
air
導入済み
設定のざっくり解説
Docker
今回のリポジトリはecho
で作られたGoのアプリケーションサーバーがDocker
上で実行されています。
開発環境には後述するair
やdelve
を使うためにそれ用の記述がされたDockerfile.dev
を使用します。
FROM golang:1.23 AS builder
WORKDIR /app
# airとdelveのインストール
RUN go install github.com/air-verse/air@latest && \
go install github.com/go-delve/delve/cmd/dlv@latest
RUN \
go mod download -x
RUN \
go build -o /bin/myapp
COPY . .
# airの実行
CMD ["air", "-c", ".air.toml"]
ローカル上から、Dockerコンテナで動作してるアプリケーション実行時にブレークポイントを刺したいので、compose.yaml
の設定をいじって、ローカル上の2345ポートをコンテナ上の2345ポートにバインドしておきます。
services:
app:
build:
context: .
dockerfile: Dockerfile.Dev
ports:
- '${PORT:-3333}:${PORT:-3333}'
- '${DEBUG_PORT:-2345}:${DEBUG_PORT:-2345}' # ここを追記
env_file:
- .env
volumes:
- .:/app
airとdelve
Goで開発するとき、都度Buildし直すのは大変なので、airを使用されている現場も多いのではないでしょうか?
air
は、ファイルに更新が走るたびにソースコードをBuildし実行してくれる、いわゆるホットリロードを導入するツールです。
go install github.com/air-verse/air@latest
を実行してairをインストールしたあと、air init
をすることで生成されるair.toml
を以下のように修正しています。
root = "."
tmp_dir = "/tmp"
[build]
args_bin = []
bin = "/bin/myapp"
cmd = "go build -gcflags=\"all=-N -l\" -o /bin/myapp ."
full_bin = "dlv --headless=true --listen=:2345 --api-version=2 --accept-multiclient exec --continue /bin/myapp"
---以下自動生成されたままなので省略---
詳細は割愛しますが、cmd
でGoをBuildする際のコマンド、full_bin
でアプリケーションを実行する際のコマンドが書かれている事がわかると思います。
今回はデバッグツールのdelveを使用するため、アプリケーションの実行時にdlv exec
コマンドを使用して、Goアプリケーションを実行しています。
VSCode
VSCodeのデバッガー起動時の設定ファイル、launch.json
はこちら。
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Server",
"type": "go",
"request": "attach",
"mode": "remote",
"debugAdapter": "dlv-dap",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/app"
}
],
"port": 2345,
"host": "localhost",
"showLog": true,
"trace": "verbose"
}
]
}
ただデバッガーを動かしたいだけなら、前述までの設定をして、デバッガーをF5キーを使って実行して、アプリケーションを呼び出せばブレークポイントで処理が止まるはずです。
こちらのデモでは、3回目のcurl
時にブレークポイントを貼った後にリクエストを飛ばしたため、ブレークポイントの時点で処理が止まっています。
この記事の以下は、この設定に至るまでの格闘のログです。
苦しみpoint
いろいろと調べた結果、僕が最初にかいたlaunch.json
は以下です。
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Server",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app",
"port": 2345,
"host": "localhost",
"showLog": true,
"trace": "verbose"
}
]
}
この状態でも、まぁなんとなく動いてはいたのですが様々問題を抱えていました。
問題は以下です。
- ブレークポイントで処理が止まらないことがある(止まることもある)
- デバッガーを落としても、引き続きブレークされ続ける
色々と格闘して、デバッガーとして致命的な1つ目は解消できたので、詳細な症状と解消法を記載します。
ブレークポイントで処理が止まらないことがある(止まることもある)
致命的でした。
- コンテナが動いている
- curlをするとレスポンスが返ってくる
- ホットリロードも効いている
といったアプリケーションが完全に動作している状態でも、何故かブレークポイントで処理が停止しないことがありました。
そして、1回目のリクエストで停止しなかった箇所でも、2回目以降のリクエスト時は意図通り刺さることがあり、挙動として不安定なところも問題でした。
論より証拠ということで以下がデモの様子です。
↑のgif画像では、デバッガー起動時に合計6箇所のブレークポイントを貼っていますが、1度目のcurl
では4個所しかブレークされていません。そして、2度目以降のcurl
の際は6箇所すべてのブレークポイントで停止がされていることがわかると思います。
色々と調べていくと、vscode-go(VSCodeのGolang拡張)のドキュメントにたどり着きました。
そこでremotePath
という設定がdeprecatedになり、代わりにsubstitutePath
という設定が推奨となっていたことに気づきました。
設定を以下のように書き換えます。
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Server",
"type": "go",
"request": "attach",
"mode": "remote",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/app"
}
],
"port": 2345,
"host": "localhost",
"showLog": true,
"trace": "verbose"
}
]
}
substitutePath
の説明には、以下のように書いてあります。
ローカルパス(editor)からリモートパス(debugee)へのマッピングの配列。 この設定は、シンボリックリンクのあるファイルシステムで作業したり、 リモートデバッグを実行したり、外部でコンパイルされた実行可能ファイルをデバッグ したりする場合に便利です。 デバッグ・アダプタは、すべてのコールでローカル・パスをリモート・パスに置き換えます。 remotePath によってオーバーライドされます。
実は、launch.json(ver.2)
のまで状態では、launch.json(ver.1)
と挙動は変わりません。deprecatedのものを使わなくて気持ちよくなっただけです。
しかしながら、ここで"debugAdapter": "dlv-dap"
という設定を加えると状況が変わりました。
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Server",
"type": "go",
"request": "attach",
"mode": "remote",
"debugAdapter": "dlv-dap",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/app"
}
],
"port": 2345,
"host": "localhost",
"showLog": true,
"trace": "verbose"
}
]
}
DAP(Debug Adapter Protocol)
はVSCodeのでデバッグ機能の裏で動いている通信プロトコルです。以前はdelve(Goのデバッガー)
はDAP
に対応していなかったため、VSCodeはGoのデバッグの際にはDAP
の通信をDebugAdapter
という機構で中継してやり取りするような通信モデルを採用していていました。これが、"debugAdapter": "legacy"
という設定です。
しかしながら、delve
がDAP
に対応した事によりこちらの通信モデルは過去のものとなり、今ではVSCodeのデバッグ機能が直接DAP
プロトコルを使ってdelve
とやり取りできるようになっています。こちらが"debugAdapter": "dlv-dap"
という設定になります。
出典: Go debug extension architecture overview(https://github.com/golang/vscode-go/blob/master/docs/debugging.md#go-debug-extension-architecture-overview)
すでに起動されているGoのサーバーのデバッグを行う際、launch.json
では、"request": "attach"
を使います。
ドキュメントを読んだところ、"request": "attach"
際にdebugAdapter
はlegacy
がデフォルトの設定となっていました。
(なぜか"request": "launch"
のときは、dlv-dap
がデフォルト)
こちらを"debugAdapter": "dlv-dap"
に切り替えて、かつremotePath
ではなくsubstitutePath
を指定するようにしたところ、所望のブレークポイントでいつでも処理を止めることができるようになりました。
デバッガーを落としても、引き続きブレークされ続ける
こちらも論より証拠ということで、まずデモをしている画像を貼ります。
1回目のcurl
の時は問題ありません。デバッガーで指定したブレークポイントで、しっかりとブレークされています。
問題は2回目のcurl
の時です。2回目のcurl
をしたとき、VSCodeのデバッグ機能の実行を停止しているにも関わらず、ブレークポイントで処理が止まっている事がレスポンスが返ってきていないことからわかります。実際、F5キーなどでVSCode
のデバッグ機能を再実行したらブレークポイントで処理が止まっています。
この状況を見れば、「おそらく裏側でdelve
の実行が生きていて、処理が停止しているというのだろう」ところまでは予測できます。
実際に、ブレークポイントをすべて剥がした上でVSCode
のデバッグ機能を停止すれば、当たり前ですがブレークされなくなります。
しかしながら、
- なぜ
VSCode
のデバッグ機能の実行をやめてもdelve
は動き続けるのか? - 回避策はないのか?
については不明のままです。
一応対症療法ですが、docker restart
をすると一旦delveも再起動がかかり意図しないブレークはされなくなるですが、地味にストレスな挙動です。直したい。
おわりに
2つ目の問題が解消できていないので志半ば感はありますが、ひとまずは使い物になるデバッガーの状態にはできたと思います。
Printデバッグでも開発を進められないことはないのですが、
- 入れ子になった構造体のフィールドの値の確認
- 外部パッケージ内のデバッグで、Print関数を挟みづらい状況のとき
などで、デバッガーは強烈な価値を出すと思っています。
そういったときにいちいち悩みたくないので、開発環境を作る際はまず最初に用意したいものだなと個人的には思っています。
Discussion
参考になるかわかりませんが、たとえば Java で Tomcat で動作するアプリを Maven + cargo-maven3-plugin を使ってリモートデバッグするときは、起動用タスク、停止用タスクを用意し、それらを使って launch で起動や停止ができます。
そういった方法を参考にすればできそうだと最初のコメントには Maven + cargo-maven3-plugin の例を記載しました。さきほど Golang Air で確認したらできました。
次のようなワークスペースファイルを用意して試してみたところ、デバッグ開始で air の起動、デバッグ停止で air 停止ができました。記事ではDocker を使っているので、air-start の command を
docker compose up -d
とし、air-kill の command をdocker compose down
コマンドにすればいけるはずです。(省略しましたが docker-compose.yaml ファイルの指定も必要)ただし、再起動は、自分で「停止」「起動」とする必要があります。
最初にコメントさせていただいたコメントについて、Tomcat の例を Go Air のものに変更しました。コメント更新が通知でいくかわからないので、コメントいたします。
コメントありがとうございます。
なるほど、コンテナ立ち上げ自体をVSCodeのデバッグ機能起動と同時に行う作戦なのですね。
確かにこれならデバッグ機能停止時にも謎にブレークされる問題は生じませんね!
それにしてもlaunch.jsonでそんな詳細な設定ができるとは知りませんでした…
めちゃくちゃ勉強になりました!