Airを使ってホットリロードを導入しGoプログラミング効率を上げる
はじめに
PHP や Ruby といった LL 言語でのプログラミングでは大部分のコードの変更が即座に反映されます。
いわゆるホットリロード機能があるため、コードを変更したあとにすぐに動作確認が可能です。
Go 言語はコンパイル言語であり、 go run
を使っていたとしても実質的にはコンパイルしています。
そのため変更を反映するためには 「実行プロセスの停止」→「コンパイル」 の手順を踏む必要があります。
開発中にコード変更のたびにこれを行うのは面倒なので、ホットリロードの仕組みを導入したいと思いました。
Air というツールの情報をよく見るので導入してみました。
検証環境
$ go version
go version go1.20.2 darwin/arm64
Air とは?
Github で公開されています。
Go アプリケーション向けにホットリロード ( Live reload ) の機能を提供するツールとのことです。
開発のきっかけについて作者が述べています。
Gin フレームワークを使った Web サイトの開発を始めたとき Gin にライブリローディング機能がなく苦痛を感じたということです。
Fresh というツールを見つけたそうですが、柔軟性がなさそうと思い、より良いものを自作することにしたそうです。
特徴
- ログをカラフルに出力してくれる
- ビルドだけでなく、その他のコマンドも実行可能だ
- 特定のディレクトリを無視することができる
- Air が起動したあとに追加されたディレクトリも自動的に監視してくれる
インストール方法
Go 1.16 以上を導入している場合は、いつものように go install
を使うのが良いでしょう。
$ go install github.com/cosmtrek/air@latest
ちなみに Docker イメージ も提供されています。
使ってみる
作業ディレクトリで Air を使ってみます。
$ mkdir -p work
$ cd work/
最も s ンプルな起動コマンドが公式ページに書かれていました。
実行してみます。
$ air -c .air.toml
以下のエラーが表示されました。
$ air -c .air.toml
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
2023/04/13 07:29:26 open .air.toml: no such file or directory
当然のことながら .air.toml
というファイルが無いと言われました。
.air.toml
ファイルを作成
.air.toml
ファイルを作成するには以下のコマンドを実行します。
$ air init
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
.air.toml file created to the current directory with the default settings
作成されたファイルを除くと思った以上に最初からボリュームのある設定ファイルとなっていました。
この状態であれば先程のコマンドが実行可能です。
$ air -c .air.toml
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
mkdir /work/tmp
watching .
!exclude tmp
building...
go: cannot find main module, but found .git/config in /work
to create a module there, run:
go mod init
failed to build, error: exit status 1
running...
/bin/sh: /work/tmp/main: No such file or directory
ちなみに -c .ari.toml
のオプションは省略可能です。air
はカレントディレクトリから .air.toml
ファイルを探し起動します。
.air.toml
設定ファイルの各項目について
設定ファイルの項目の数に圧倒されますが、ひとつひとつ調べてみました。
動作確認をしながら進めようと思うので、以下のようなかんたんな Go 言語のコードを作業用ディレクトリに作成しておきます。
# 現在、 work というディレクトリにいます
$ echo ${PWD##*/}
work
# main.go というファイル名で作成してみました
$ cat <<'EOF' >main.go
package main
func main() {
println("Hello World")
}
EOF
go.mod
、go.sum
も作成しておきます。
# モジュール名(アプリ名)は何でもいいですが、 "genzouw/introducing-air" としました。
$ go mod init genzouw/introducing-air
$ go mot tidy
また、先程実行した air init
コマンド実行直後に作成された .air.toml
ファイル(まだ変更していません)の内容は以下のようになっています。
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
air
コマンドを実行すると、 main.go
が実行され "Hello World" というメッセージが表示されることが確認できました。
$ air
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
watching .
!exclude tmp
building...
running...
Hello World
root
root
の初期状態は .
となっていました。
これを hogehoge
というディレクトリに変更してみます。
root = "hogehoge"
testdata_dir = "testdata"
tmp_dir = "tmp"
...
mkdir hogehoge
でディレクトリを作成した後、 air
を起動します。
$ air
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
mkdir hogehoge/tmp
watching .
!exclude tmp
building...
running...
Hello World
hogehoge/tmp
というサブディレクトリが作成されたようです。
$ ls -l hogehoge
drwxr-xr-x - genzouw 13 4 07:52 tmp/
# からっぽ!
$ ls -l hogehoge/tmp
main.go
の println
メッセージを変更してみましたが、変更が反映されず。
どうやらこの設定は、監視対象のディレクトリのルートを指定するもののようです 。
hogehoge
に hello.go
というファイルを作成してみました。
cat <<'EOF' >main.go
package main
func main() {
println("Hello genzouw")
}
EOF
air
のログが出力(変更が検知)されましたが、メッセージは Hello World
でした。
つまり変更検知はしたが、ビルドコマンド (go build
)実行ディレクトリやビルド対象のファイル main.go
は変わらないようです。
tmp_dir
こちらはわかりやすいですね。
air
コマンド実行後、プロジェクトルートディレクトリには tmp/
というディレクトリが作成されます。
中には以下のようなファイルが格納されていました。
$ ls tmp
build-errors.log main*
この main
というファイル、実行可能なファイルだったので実行してみました。
$ tmp/main
Hello World
main.go
がビルドされた実行ファイルでした。
tmp_dir
はビルドのログやビルド実行結果ファイルが出力されます、文字通り「一時的なディレクトリ」を設定するもののようです。
試しに値を変更してみました。
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp_"
...
air
で起動し、 tmp_
を覗いてみます。
# なにも見つからない...
$ ls -l tmp_
おかしい、何も見つかりません。tmp_dir
の値を変更したからと言って、ビルド結果の実行ファイルやログの出力先まで変更されるわけではないようです。
ということで、ここまで来て tmp_dir
の値はほぼ変えることはないか、変える場合は他の項目もセットで変更が必要なことがわかりました。
bin
初期の設定値を見て、ここの意味はおおよそ想像が付きました。
...
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
...
ファイルの変更が検知されたとき、 cmd
が実行され、 その後 bin
のコマンドが実行されると予想されます。
つまりこちらの設定値は任意のものを指定できそうです。
試しに書き換えてみます。
まずは bin
から。
以下に書き換えてみました。
...
[build]
args_bin = []
bin = "echo 'hello hello'"
cmd = "go build -o ./tmp/main ."
...
air
の実行結果は以下の通りでした。
/bin/sh: /work/echo: No such file or directory
なるほど。
.air.toml
設定ファイルのは位置ディレクトリからの相対パスにしないといけない- 任意の「コマンド」ではなく、「実行ファイル」を指定しないといけない
ということはプロジェクト配下に置かれたシェルスクリプトは実行できそう。
$ cat <<'EOF' >hello.sh
$ echo hello hello hello
EOF
$ chmod +x hello.sh
bin
の設定は以下のように変更してみます。
[build]
args_bin = []
bin = "./hello.sh"
これで main.go
の変更を検知して hello.sh
スクリプトが実行されるようになりました!
cmd
bin
で設定した実行ファイルを生成(ビルド)するためのコマンドを設定する項目だということがわかります。
bin
と違って、ここはプロジェクトルートディレクトリに置かれている「実行ファイル」や「スクリプト」である必要はなく、任意のコマンドが設定できそうです。
Go のモジュールによってはビルドを用意に実行できるように Makefile
を提供してくれているものもあるので、 cmd = "make"
なんて設定も有効そうです。
cmd
の設定を以下のようにしてみました。
cmd = "date >> date.log; go build -o ./tmp/main ."
うまく行けば、 main.go
変更のたびに date.log
に現在の時刻が追加されるはずです。
air
を起動し直してみると、想定通り date.log
が作成されていました!
$ cat date.log
木 4 13 08:34:20 JST 2023
main.go
を一度書き換え、保存した後、もう一度 date.log
を覗いてみます。
$ cat date.log
木 4 13 08:34:20 JST 2023
木 4 13 08:34:37 JST 2023
日付情報が追記されていますね!
実際の利用シーンを想像してみると、 make
やビルドスクリプトのようなものを使わなかった場合、つまり直接 go build
を記述した場合 -o
で指定した生成される実行ファイルのパスと bin
フィールドに設定されるパスは一致するはずです。
(一致しないとビルド実行後にリローディング対象の実行ファイルが見つかりません)
では、 bin
フィールドを消したらどうなるのでしょうか?
試しに消して air
を起動してみると、これでも問題なく動作しました。
どうやら bin
が設定されていない場合には /tmp/main
を探し出して実行しているようです。
ビルドされる実行ファイル名は ./tmp/main
である必要はないので、 cmd
と bin
をセットで変更し別の名前にすることもできます。
# 実行ファイル名を tmp/hello としてみる
bin = "tmp/hello"
cmd = "go build -o ./tmp/hello ."
full_path
こちらは bin
フィールドに対してプロジェクトルートからの相対パスに置かれているコマンドあるいはスクリプトしか設定できないです、という制約を超えるためのフィールドです。
正直 full_bin
を使えばなんでもできそうでした。
full_bin = "echo 'hogehoge'"
$ air
__ _ ___
/ /\ | | | |_)
/_/--\ |_| |_| \_ , built with Go
watching .
!exclude tmp
building...
running...
hogehoge
実行時に環境変数を設定します、といったような用途で使われる想定のようです。
bin = "APP_END=dev ./tmp/main"
本当になんでも設定できるので、以下のような設定も可能です。
full_bin = "echo 1; echo 2; echo 3"
cmd
フィールドで go build
やら go vet
やら go test
を行ってアプリケーションの実行は特にしないです、という使い方もできそうです。
# アプリケーションの実行はしない
full_bin = "true"
cmd = "make"
rerun
デフォルトでは rerun = false
となっている項目ですが、こちらを以下のように変更してみます。
rerun = true
air
を起動し直すと、エンドレスで Hello World
が表示され続けます。
$ air
watching .
!exclude tmp
watching tmp_
building...
running...
Hello World
Hello World
Hello World
Hello World
Hello World
実行後にすぐに終了するようなコマンドラインツールの場合は不要でしょう。
サーバーのような起動し続けてイベントトリガーで処理を行うようなプログラムの場合、実装の不具合で停止してしまうたびに手動で起動し直すのは面倒ですので、 true を設定しておくと良さそうです。
ひとこと
最低限、今回取り上げたぐらいの設定項目を知っていれば順分利用できそうです。
その他の項目は include
OR exclude
観点の設定項目が多く、変更を監視するファイルをフィルタリングする設定項目となっています。
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
ディスカッション
コメント一覧
まだ、コメントがありません