Goで快適な開発環境を手に入れるためにホットリロードツールを開発している話

この投稿は、 [Go4 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go4)の 13日目のものになります。

# はじめに
例えばWebアプリケーションを開発する際などに、ソースコードの変更を検知して自動で再ビルドをしアプリケーションを起動する、所謂ホットリロードを行う機能はだいたいのメジャーな言語で何かしらの形で提供されていると思います。

Goにもいくつかのライブラリが存在していて、おそらくメジャーなものといえば [fresh](https://github.com/gravityblast/fresh) と [realize](https://github.com/oxequa/realize) の2つだと思います。
僕自身も`fresh`には数年ほどお世話になっており、実際にプロジェクトの開発でも使用しています。
しかしながら、`fresh` を使用していく中でこのライブラリがいくつかの問題を抱えていることもわかりました。

- メンテナンスがされていない
- `mod=vendor` のような仕組みに対応しきれていない (https://github.com/gravityblast/fresh/issues/99)
- 監視ファイル(ディレクトリ)の細かい設定ができない
- 起動時の最初のbuildに失敗すると監視が動かない

`realize` もこのうちいくつかの同様な問題を抱えており、特に監視ファイルの柔軟な設定ができないのが比較的大きいプロジェクトを効率よく開発していく上でストレスになりました。

また、ローカルの開発環境を `Docker` や `docker-compose` で構築することは最近では一般的な手段の一つだと思います。その際 `Mac` で `Docker` を利用する場合は、 `Docker for Mac` を利用することが多いと思われますが、処理が遅くホットリロードを行うと応答しなくなることも稀によくあり、これもまた開発効率を落とす要因であることに気づきました。

さきほど紹介したライブラリは、メンテナンスされていないこともありこれらの問題に対する解決をライブラリ側に望むことはほぼ不可能だと考えています。
そのためこれらの代替手段を探していましたが、なかなか見つからなかったため今回は実装することにしました。

本記事は僕が開発しているホットリロードツール `fresher` についての紹介記事になります。
アドベントカレンダー投稿日にリリースが間に合わずまだ開発中ではありますが、https://github.com/kanataxa/fresher というレポジトリで公開しています。
`realize` ほど多機能でないシンプルなホットリロードを提供するライブラリですが、 `fresh` が抱えている課題を解決した `better-fresh` を目指しています。

※開発途中のため仕様変更が入る場合があります、ご了承ください

# fresher（開発中)が満たす機能について
後発である `fresher` を多くのユーザに使ってもらうためには、 今まで存在していた従来のツールと比較した際に優位な点を示すこと、そして学習コストを低くし乗り換えをしやすくする必要があると考えました。

そこで、 `fresher` では以下のような機能を提供します。

## YAMLベースの設定ファイルによる機能のカスタマイズ
`fresher` では細やかな設定をしてくためにユーザ側に設定ファイルを書くこと要求します。
もし手っ取り早く `go run main.go` をホットリロードで起動したい場合は以下のような設定を書いて `fresher -c your/conf/path.yml` を実行するだけです。

```yaml
path:
  - .
```
この設定ファイルをユーザがカスタマイズすることで適用するプロジェクトへより最適化できる機能を提供することを目指しています。


## ホットリロード
`fresher` の一番核となる機能です。
`fresher` では, https://github.com/fsnotify/fsnotify を用いてファイルの監視をしています。
`fresher` はファイルの作成も検知しなければならないため、監視対象のファイルではなくディレクトリ丸ごとを監視します。
そして `fsnotify` によって通知されたファイルが監視対象であった場合に、再度 `go build` し、ビルドしたバイナリを実行することでホットリロードを実現しています。

`go run` を使用していない理由は、後述のビルドする環境と実行環境を分けるような機能を提供する都合と、`go run` 内で作成されたバイナリを実行するプロセスが別で起動してしまうため、別途そのプロセスを `kill` しなければならないためです。

## 監視ファイルの設定に柔軟性を持たせる
一番最初に述べたように `fresh` は監視するファイルやディレクトリの指定に柔軟性がないと考えています。
例えば、特定のディレクトリの配下の特定のファイルパターンを無視したいときなど、これを指定する方法がありませんでした。
そこで `fresher` ではより柔軟に監視ファイルの設定をできるようにします。
具体的には下記の仕様をサポートします。

1. ディレクトリごとに監視/無視するファイルやディレクトリを指定できるようにする
2. グローバルに無視するファイルやディレクトリを指定できるようにする
3. パターンマッチングを用いて指定できるようにする

まずは以下のディレクトリ構成と `yml` ファイルをみてください

```bash
bash-3.2$ tree ./
./
├── config.yml
├── main.go
├── controller
│   ├── controller.go
│   └── const.go
├── model
│   ├── message.go
│   └── message_test.go
└── pkg
    ├── config.go
    ├── const.go
    ├── pkg.go
    └── utils
        ├── utils.go
        └── utils_test.go

```

```yaml
path:
  - .
  - name: controller
    exclude:
      - const.go
  - model
  - name: pkg
    include:
      - utils*
      - config.go
exclude:
  - vendor
  - '*_test.go'
```

`path` というフィールドでディレクトリを指定し `include` と `exclude` によってそのディレクトリ配下にあるファイルやディレクトリの監視/無視の設定をします。もし `include/exclude` を特に指定する必要がない場合は `- dir_name` のような記述も可能になっています。

また `path` と同じレベルにある `exclude` によってグローバルに無視するファイルやディレクトリの設定ができます。

基本的に以下のようなルールで設定ファイルから監視対象のファイルを決定します。
1. `path` に記述されたディレクトリを起点に再帰的に監視対象のファイルかどうかを判断する
2. `include` が指定されている場合は、再帰的に見るディレクトリや監視対象とするファイルを `include` で指定されたものと一致するもののみにする. もしくは `exclude` に設定されているファイルと一致した場合は監視対象から除外する。
3. `include/exclude` はファイルかディレクトリかを区別せず `filepath.Match` を用いて一致したものを監視/除外する。

## クロスコンパイル可能
Goはクロスコンパイル可能な言語です。そこで `fresher` でもクロスコンパイルをサポートします。
`env` というフィールドを用いて `GOOS` や `GOARCH` を指定できるようにします。
もしユーザが `fresher` を実行している環境と別の環境のバイナリをビルドしたい場合はこのフィールドを設定することでビルドが可能になります。
また `arg` や `output`, `target` を指定することでビルド対象やバイナリの出力先、ビルド時の引数も指定することが可能です。

```yaml
build:
  env:
    - GOOS=linux
  target: main.go
  output: /tmp/app
  arg:
    - -v
```

## ビルド前後でのフックの提供
`fresher` では `build` の直前と直後にユーザが任意の操作をできるような機能を提供しています。これは `realize` が提供していた `scripts` に似たような機能です。
`build` 内の `before/after` で実行する操作を設定することができます。 `async` でその実行処理の完了を待つかどうかを設定することが可能です。

```yaml
build:
  before:
    - echo Before Build hook
  after:
    - docker cp /tmp/app container:/tmp/app
    - name: docker
      arg:
        - exec
        - container
        - /tmp/app
      async: true
```

## ホスト上でビルドし、Dockerで実行する(開発中)
例えば、`Mac` で `Docker` を使用する際に `Docker For Mac` 使うことがあるかと思いますが、非常に動作が遅く重いです。
この場合、ホットリロードを `Docker` 側で行おうとすると、ホスト上で数秒でビルドできるようなソースコードですらビルドにものすごく時間がかかる場合があります。これは開発サイクルに大きな支障をきたしストレスの原因になります。

そこで、 `fresher` ではホスト上でビルドしDocker側で実行できるような仕組みを提供することでより快適な開発環境をユーザに提供します。

※この仕組みは現在開発中です。

# おわりに
この `fresher` は、ユーザの求める機能をなるべくシンプルでより使いやすく提供するということを目指しながら開発しています。
また、`Go` のホットリロードツールの新たな選択肢の一つになればよいなと考えています。

まだ開発途中ではあるものの、おおよその機能は開発完了で設定ファイルの書き方をよりブラッシュアップしていくという状態で、年内~年始あたりに`v0.0.1` をリリースできればよいなあと考えています。
 `fresh` が提供していた基本的なホットリロードの機能はすでに実装されていて、`fresh` 相当のツールとしては利用可能だと思います。

特に監視ファイルの細かい指定ができないことは `fresh` を使用していて課題に感じていたので、もし同様の思いを抱えているがいたらぜひ使っていただけると嬉しいです。

また、`Docker For Mac` などを用いて開発していて再ビルドのストレスに悩まされている方、まだ開発中ですが、なるべく早く(遅くても年内には)この問題への解決策の一つを `fresher` が提供する予定なので提供した際には良ければ利用してみてください。

そして、もし使用するうえで要望や問題があった場合は[Issue](https://github.com/kanataxa/fresher/issues)を立ててください、日本語で大丈夫です。なるべく早いメンテナンスをします。