Docker+Go+Airの開発環境がうまく動かないのでゴリ押しした

ある日、Goを含むweb開発環境のDockerイメージを作ろうと思い立ちました。
実際に作成しようとしたのはnode.jsとGoの両方を含む開発環境なのですが、Goの部分でつまづいたところがあったので解決（したことにする）案を共有します。

想定する流れは以下の通りです。

1. Dockerイメージを起動
2. VS Codeでファイルを編集（Dockerコンテナの外側）
3. Dockerコンテナ内側のAirでホットリロード

DockerもGoのセットアップも慣れているので、組み合わせるだけですぐできるだろうと思っていました。

# つまづいたところ
Airがホットリロードしません。

`docker exec`を使用して内側から`echo "" >> main.go`などの変更を加えるとホットリロードします。
どうやらコンテナの外側からのファイル変更イベントがうまく走ってないようです。

# 環境
現象を確認したのは以下の環境です。
Macを使っている方やGo公式イメージを使用されている方の中にはAirがうまく動作している方もいるようなので、筆者の環境がよくない説はあります。

* PC環境
    * OS
        * Windows 10 Pro 22H2
    * メモリ
        * 16GB
    * Docker
        * version 20.10.14
    * エディタ
        * VS Code 1.75.1
* 仮想環境（Docker内）
    * OS
        * debian stable-20230202
    * Go
        * version 1.20
    * Air
        * version 1.41.0

# 解決（したことにする）案
Windows環境ではAirを使うのはやめて、`PowerShellでファイルの変更を検知して、外側から通知する`ようにしました。

「それだと開発環境のセットアップをDockerで完結できるというメリットがなくなっちゃわない？」というド正論はあるのですが、以下二点の理由により容認できると考えています。

* WindowsであればPoweshellは標準搭載なので、複雑な構成にはなるが環境移行の手順は増えない
* 本番環境のイメージを作ってるわけでもないのに、時間をかけるのはもったいない

## Docker内のLinuxに配置するシェル

Dockerfileのentrypointには以下のようなスクリプトを指定します。
環境変数`DEVELOP_BUILD`に`GORIOSHI`が入ってきたら、Airを使わずにゴリ押しでホットリロードします。


```entrypoint.sh
#!/bin/bash

export PATH=$PATH:/develop/app/go/bin
export GOPATH=/root/go/bin
export PATH=$PATH:/root/go/bin

if [ "$DEVELOP_BUILD" == "AIR" ]
then
  # Airでホットリロード
  echo Go Airで起動します
  echo ソースコードは自動でホットリロードされます
  air -c .air.toml
elif [ "$DEVELOP_BUILD" == "GORIOSHI" ]; then
  # ゴリ押しホットリロード
  # Airが意図したとおりに実行されない場合にはこれをつかう
  echo ゴリ押しデバッグモードで起動します
  echo ソースコードはビルドされ続けます
  for ((i=0; 1 == 1; i++)); do
      # ビルド＆実行
      bash /develop/script/go/run.sh
      # 実行が終了すると1秒待って再度ビルド＆実行しつづける
      sleep 1
  done
else
  echo "環境変数'DEVELOP_BUILD'の値'${DEVELOP_BUILD}'を識別できません"
  sleep 5
  exit 1
fi

```

プロセスを終了するスクリプトを配置します。

```kill.sh
#!/bin/bash

# 実行しているプロセス「main」を終了するスクリプト

pid=`pidof main`
if [[ "$pid" != "" ]]; then
    echo "プロセスID$pidを停止しています..."
    kill $pid
fi

```

ソースコードをビルドして起動するスクリプトを配置します。
`tmp`フォルダの中に`main`という名前の実行ファイルを生成して実行します。

```run.sh
#!/bin/bash

# Goのソースコードをビルドして実行するスクリプト

#プロセスを終了
bash /develop/script/go/kill.sh

# フォルダ作成
if [ ! -d ./tmp ]; then
    mkdir ./tmp
fi

# ビルド
echo ビルドしています...
go build -o ./tmp/main .

echo 起動します...
./tmp/main

```

## Windows側で使用するスクリプト
3秒ごとに拡張子`.go`と`.html`の更新日時の変化を監視して、変化があった場合はDockerコンテナの`kill.sh`を叩くように命令を送ります。
コンテナ内では`main`のプロセスが停止すると、`entrypoint.sh`のループが一周して、再度`run.sh`が実行されます。

※PowerShellならファイル変更イベントをトリガーにすることもできますが、以前に別件で使用した際スパゲティコードになったので今回は不採用

```watch.ps1

$path = ${PSScriptRoot}
cd $path
$list = ''

while ($true) {
    $prelist = $list
    $list = (ls "${path}\src" -r | ?{$_.Extension -in '.go','.html'} | %{ "$($_.Name) $($_.LastWriteTime)" })
    if ("$list" -ne "$prelist") {
        $ErrorActionPreference = "silentlycontinue"
        docker-compose exec develop bash /develop/script/go/kill.sh
        $ErrorActionPreference = "continue"
    }
    sleep 3
}

```

# 完成
以下のようにホットリロードができるようになりました。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884998/20567a63-7ffe-711e-1490-36a9fe4b18d3.png)

## Dockerイメージ
DockerHubにイメージをアップロードしたほか、GitHubにサンプル環境をアップロードしました。
`git clone`していただいて、`serve.cmd`を実行していただければホットリロード用のウィンドウとコンテナのウィンドウが開きます。
`src`フォルダ内のコードを改変するとホットリロードします。

[https://github.com/HoppingGanon/gorioshi-air](https://github.com/HoppingGanon/gorioshi-air)

以上です。

本当にゴリ押しコードばかりになっているので、根本的な原因のわかる方はコメントで教えていただけると嬉しいです。
駄文にお付き合いいただき、ありがとうございました。