[Go言語] freshでサブコマンドを扱う

この記事は、[ニフティグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nifty) 16日目の記事です。
昨日は@Takumi-Miuraで「[32bitUEFIのウルトラノートブックにubuntuをインストール](https://qiita.com/Takumi-Miura/items/a4178ae9d803a26d99b6)」でした。
私も以前、windowsXPのpcにubuntuを入れて蘇らせた(かった)経験があります。古いOSのpcを持っている人は、Linuxを入れてしまうというのも一つの選択肢ですね。

<hr>

# はじめに
皆さんはGo言語で開発を行うときにどのような環境で開発を行っていますか？
私は、Docker+[fresh](https://github.com/gravityblast/fresh)(ホットリロード)で快適なGoライフを送っています！
しかし、このfresh・・・**サブコマンドに対応しておりません**。
[プルリク](https://github.com/gravityblast/fresh/pull/92)は出ていますがマージされず・・・。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217116/5743c3f8-5ac8-a85e-920b-4b73742e0533.png)

今回は、非常にニッチな内容にはなりますがfreshでサブコマンドを使用する方法を調査したので情報を共有します。

なお、freshの基本的な使用方法や概要については、@po3rinさんの[Go v1.11 + Docker + fresh でホットリロード開発環境を作って愉快なGo言語生活](https://qiita.com/po3rin/items/9acd41ef428436335c97)や[README.md](https://github.com/gravityblast/fresh/blob/master/README.md)が参考になるのでそちらをご参照ください。

# 本編
今回は、サブコマンドを利用して複数のサーバーを立ち上げ、それぞれの環境でホットリロードできるかを確認します。
### 解決方法
オリジナルをフォークして機能を拡張している方がいたので、そちらを使わせていただきます。
[c2h5oh/fresh](https://github.com/c2h5oh/fresh)
### 構成
最終的な構成は以下のようになります。

```
.
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── main
├── main.go
├── runner.conf
└── tmp
    └── hot_reload_docker
```

### ローカルで試す
はじめに、ローカル環境で実行してみましょう。

まず、go modulesが使用できるバージョンであることを確認します。1.1以上ならOKです。

```terminal:terminal
$ go version
go version go1.12.7 darwin/amd64
```
Go modulesを有効にします。

```terminal:terminal
$ export GO111MODULE=on
```
モジュール管理ファイルを生成します。

```terminal:terminal
$ go mod init .
```
実行するファイルを記述していきます。

```main.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/mux"
	"github.com/urfave/cli"
)

func main() {
	app := cli.NewApp()

	app.Name = "nifty gopher"
	app.Usage = "Hello nifty gopher."
	app.Version = "1.0.0"

	app.Commands = []cli.Command{
		{
			Name:  "gopher",
			Usage: "Hello Gopher",
			Action: func(c *cli.Context) {
				router := mux.NewRouter()
				router.HandleFunc("/", helloGophers).Methods("GET")
				log.Fatal(http.ListenAndServe(":8080", router))
			},
		}, {
			Name:  "nifty",
			Usage: "Hello Nifty",
			Action: func(c *cli.Context) {
				router := mux.NewRouter()
				router.HandleFunc("/", helloNifty).Methods("GET")
				log.Fatal(http.ListenAndServe(":8080", router))
			},
		},
	}
	app.Run(os.Args)
}

func helloGophers(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Hello Gopher.")
}

func helloNifty(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Hello Nifty.")
}
```
以上のコードで、サブコマンドに対応したサーバーが立ち上がります。
gopherサーバーから立ち上げます。

```terminal
$ go build .
$ ./main gopher
```
別のターミナルから接続してみます。

```
$ curl http://localhost:8080
```
``Hello Gopher.``と出力されれば成功です。
また、``./main nifty``に置き換えて実行してみてください。
``Hello Nifty.``と出力されれば、サブコマンドによってサーバーを指定できるcliツールの完成です。

### Dockerのホットリロード環境で実行する
いよいよDockerのホットリロード環境で実行します。
必要なファイルを記述しましょう。

#### Dockerfile

```Dockerfile
FROM golang:1.13

WORKDIR /go/src/hot_reload_docker
COPY . .
ENV GO111MODULE=on

RUN go get github.com/c2h5oh/fresh
ENTRYPOINT ["fresh","-c","runner.conf","-r"]
```
重要な部分のみ説明します。

以下のコマンドでパッケージをダウンロードします。
今回は、フォークされたパッケージを利用します。

```Dockerfile
RUN go get github.com/c2h5oh/fresh
```
最後にfreshを利用して実行を行います。

```Dockerfile
ENTRYPOINT ["fresh","-c","runner.conf","-r"]
```
オプションについて説明します。

- -c・・・読み込む設定ファイルを記述します。設定ファイルについては後述します。
- -r・・・ビルドされたバイナリに渡されるサブコマンドを記述します。実際に指定するコマンドは、docker-compose.ymlの方に記述します。

他のオプションのについては[README.md](https://github.com/c2h5oh/fresh/blob/master/README.md)に記載されていますので、ご参照ください。

#### docker-compose.yml

```docker-compose.yml
version: '3'
services:
  gopher-server:
    build: .
    volumes:
      - ./:/go/src/hot_reload_docker
    ports:
      - "8080:8080"
    command: "gopher"
  nifty-server:
    build: .
    volumes:
      - ./:/go/src/hot_reload_docker
    ports:
      - "8081:8080"
    command: "nifty"
```
``commandタグ``によって、サブコマンドを指定します。

```docker-compose.yml
command: "gopher"
```
今回は、``gopher``、``nifty``をそれぞれ実行します。

#### runner.conf(設定ファイル)

```runner.conf
root              = "."
watch_paths       = []
exclude_paths     = []
tmp_path          = "./tmp"
output_binary     = "."
build_log         = "runner-build-errors.log"
valid_ext         = [".go", ".tpl", ".tmpl", ".html"]
build_delay       = 600
colors            = true
log_color_main    = "cyan"
log_color_build   = "yellow"
log_color_runner  = "green"
log_color_watcher = "magenta"
log_color_app     = ""
```
一部抜粋して説明します。

以下の記述で、実行するファイルが存在するディレクトリを指定します。

```runner.conf
root = "."
```
プログラムの変更があった時に自動でビルドが走りますが、変更を監視するディレクトリを指定します。

```runner.conf
watch_paths = []
```
生成されたバイナリファイルのディレクトを指定します。

```runner.conf
output_binary = "."
```
監視対象にする拡張子を指定します。

```runner.conf
valid_ext = [".go", ".tpl", ".tmpl", ".html"]
```

#### 実行
必要なファイルが揃ったので実行していきます。

ビルドから。

```terminal:terminal
$ docker-compose build
Building gopher-server
・・・
Successfully tagged fresh-subcommand_gopher-server:latest
Building nifty-server
・・・
Successfully tagged fresh-subcommand_nifty-server:latest
```
一部省略していますが、以上のようなメッセージが出力されれば成功です。

立ち上げます。

```terminal:terminal
$ docker-compose up
・・・
gopher-server_1  | 8:29:44 main        | Waiting (loop 2)...
・・・
nifty-server_1   | 8:29:44 main        | Waiting (loop 2)...
```

別のターミナルで接続してみましょう。

```terminal:terminal
$ curl http://localhost:8080
$ curl http://localhost:8081
```
立ち上げに成功していることが確認できました。

```terminal:terminal
gopher-server_1  | 8:32:34 app         | Hello Gopher.
nifty-server_1   | 8:32:40 app         | Hello Nifty.
```

出力する文字列を変更してみましょう。

```main.go
func helloGophers(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Hello Gopher!!!!")
}

func helloNifty(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Hello Nifty!!!!")
}
```
保存すると、自動でビルドが走ります。
数秒置いてから、再び確認してみましょう。

```terminal:terminal
$ curl http://localhost:8080
$ curl http://localhost:8081
```
出力が変更されていることが確認できました。

```terminal
gopher-server_1  | 8:39:13 app         | Hello Gopher!!!!
nifty-server_1   | 8:39:14 app         | Hello Nifty!!!!
```

# まとめ
今回は、freshでサブコマンドを使用する方法を紹介しました。
フォークされたリポジトリを利用するということは初めてでしたが、誰かが作ったものに対して機能を追加できるOSSはとても面白いですね。
これで、快適なGoライフを継続できそうです！

<hr>

明日は@fuku710です。お楽しみに！