ボイラプレート編 - #golang で CLI 作るときにいつもつかうやつ

[技術選択編](https://qiita.com/izumin5210/items/7604f290785bc3f8f86a) が軽バズりして嬉しかったので続編．


## TL;DR

- 便利ライブラリ & CLI つくったよ
    - https://github.com/izumin5210/clig
    - clig を見れば @izumin5210 が普段どうやって CLI を開発しているかがわかるよ
    - いつも使ってる `Makefile` や `.travis.yml` もあるよ
- 開発用ツールの依存は [gex](https://github.com/izumin5210/gex) で管理してるよ


## logging

### [**zap**](https://github.com/uber-go/zap)
`Blazing fast, structured, leveled logging` のとおり，はやくて構造化データを吐けてログレベルも設定できるロガー．これは知ってる人も多いハズ．

自分が使うときはデバッグフラグを定義しておき，`cobra.OnInitialize` で logger を初期化して global logger にセットしている．

```go
cobra.OnInitialize(func() {
	zap.ReplaceGlobals(verboseLogger)
})
```

`ReplaceGlobals` でセットした Logger は `zap.L()` で取り出せるので，あとはコード中の任意の場所で `zap.L().Error("failed to open file", zap.Error(err), zap.String("path", path))` とかできる．

ちなみに，`ReplaceGlobals` を呼ばないと [Nop logger が利用される](https://github.com/uber-go/zap/blob/v1.9.1/global.go#L42)ので，テストとかに影響を及ぼすことはない．便利．



### verbose logger / debug logger
自分はだいたい `--verbose` と `--debug` の2種類のフラグを用意しておいて，それによって logger を使い分けている．

`-v` もしくは `--verbose` で `INFO` level までのログを出す Logger を使う．これは一般ユーザでも使う想定で，みやすいログ形式でそこそこの情報を出すようにしている．


```go
cfg := zap.NewDevelopmentConfig()
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
	enc.AppendString(t.Local().Format("2006-01-02 15:04:05 MST"))
}
```

一方で `--debug` は主に自分が使うものなので，すべてのログ（`DEBUG` level）を出力している．`ProductionConfig` を利用すると JSON でログが出るようになるので，それをおもむろに `jq` に食わせてデバッグしたりする．


```go
cfg := zap.NewProductionConfig()
cfg.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
cfg.DisableStacktrace = true
```

[ery](https://github.com/srvc/ery/blob/v0.0.1/pkg/app/proxy/manager.go#L20-L25) では各モジュールのインスタンスに named logger をもたせておいて，それを呼ぶようにしている．これでより `jq` しやすくなる．

```go
// https://github.com/srvc/ery/blob/v0.0.1/pkg/app/proxy/manager.go#L20-L25
return &serverManager{
	mappingRepo:     mappingRepo,
	factory:         factory,
	cancellerByPort: new(sync.Map),
	log:             zap.L().Named("proxy"),
}
```

`DEBUG` level のログは「ユーザは普段見ない」「JSON で出てくるので多少量が多くても目的のログを検索しやすい」ので，開発中の print debug 代わりに使って消さずに残しておくくらいでいいのかなと考えている．


### フラグハンドリング

いつも↓みたいな感じのヘルパを定義して，フラグを定義し，アプリケーション起動タイミングで logger も初期化している．


```go
func AddLoggingFlags(cmd *cobra.Command) {
	var (
		debugEnabled, verboseEnabled bool
	)

	cmd.PersistentFlags().BoolVar(
		&debugEnabled,
		"debug",
		false,
		"Debug level output",
	)
	cmd.PersistentFlags().BoolVarP(
		&verboseEnabled,
		"verbose",
		"v",
		false,
		"Verbose level output",
	)

	cobra.OnInitialize(func() {
		switch {
		case debugEnabled:
			enableDebugLogger()
		case verboseEnabled:
			enableVerboseLogger()
		}
	})
}
```

ref: [pkg/cli/logging.go at master · izumin5210/clig](https://github.com/izumin5210/clig/blob/835dd3977c4130ddddb86d178c134145c6f4e70d/pkg/cli/logging.go)


## stdio
たとえば `kubectl` の実装を読むと，かなり最初の方でコンストラクタから標準入出力を注入している．

> ```go
> // NewDefaultKubectlCommand creates the `kubectl` command with default arguments
> func NewDefaultKubectlCommand() *cobra.Command {
> 	return NewDefaultKubectlCommandWithArgs(&defaultPluginHandler{}, os.Args, os.Stdin, os.Stdout, os.Stderr)
> }
> 
> // NewDefaultKubectlCommandWithArgs creates the `kubectl` command with arguments
> func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
> 	cmd := NewKubectlCommand(in, out, errout)
> ```
>
> <cite>[pkg/kubectl/cmd/cmd.go#L295-L303 at v1.13.1 · kubernetes/kubernetes](https://github.com/kubernetes/kubernetes/blob/v1.13.1/pkg/kubectl/cmd/cmd.go#L295-L303)</cite>

これをやっておくだけで，テスト時は `os.Std*` の代わりに `new(bytes.Buffer)` でモックできるようになる．

自分も CLI 実装時はこのやり方を踏襲していたが，最近はもう一段ラッパーを噛ませている．

```go
type IO interface {
	In() io.Reader
	Out() io.Writer
	Err() io.Writer
}
```

`io.Writer` や `io.Reader` だとあまりに広すぎるので，もうちょっと意味をもたせる形で interface で包んでいる．この副作用として，ちゃんと型が付くので [**wire**](https://github.com/google/wire) などの型をみるタイプの DI ツールが使いやすくなる．

また，デフォルト値を返す `Stdio()` という関数を用意しておいて，`main()` からはそれを利用するようにしている．@mattn さんに [grapi の Windows 対応してもらった](https://github.com/izumin5210/grapi/pull/58/commits/9ba616d6346da1ccae907fdec4f3e4021b2896bf)ときの経験を生かして，[mattn/go-colorable](https://github.com/mattn/go-colorable) を入れている．


```go
func Stdio() IO {
	io := &IOContainer{
		InR:  os.Stdin,
		OutW: os.Stdout,
		ErrW: os.Stderr,
	}
	if runtime.GOOS == "windows" {
		io.OutW = colorable.NewColorableStdout()
		io.ErrW = colorable.NewColorableStderr()
	}
	return io
}
```

ref: [pkg/cli/io.go at master · izumin5210/clig](https://github.com/izumin5210/clig/blob/835dd3977c4130ddddb86d178c134145c6f4e70d/pkg/cli/io.go)


あとは，テスト用に[ `*bytes.Buffer` が詰まった fake 実装](https://github.com/izumin5210/clig/blob/835dd3977c4130ddddb86d178c134145c6f4e70d/pkg/cli/testing/io.go)とかを作ったりしている．


## Makefile / CI
Go でボイラプレートといえば `Makefile` と CI ですね（？）



### build

`cmd/<CLI_NAME>` 以下に `main` を置くようにしているので，それをいい感じにビルドするタスクを生成している．
たとえば `cmd/foobar/main.go` なら

- `make foobar`
    - => `go build -o ./bin/foobar ./cmd/foobar`
- `make package-foobar`
    - => `gox ... -output="dist/foobar_{{.OS}}_{{.Arch}}" ./cmd/foobar`

みたいな感じになる．こういうのも `Makefile` でよくわかんないことせずに Go でツールを作ってあげるといいのかもしれない…．


```makefile
SRC_FILES := $(shell go list -f '{{range .GoFiles}}{{printf "%s/%s\n" $$.Dir .}}{{end}}' ./...)
BIN_DIR := ./bin
OUT_DIR := ./dist
GENERATED_BINS :=
PACKAGES :=

XC_ARCH := 386 amd64
XC_OS := darwin linux windows

define cmd-tmpl

$(eval NAME := $(notdir $(1)))
$(eval OUT := $(addprefix $(BIN_DIR)/,$(NAME)))

$(OUT): $(SRC_FILES)
	go build $(GO_BUILD_FLAGS) $(LDFLAGS) -o $(OUT) $(1)

.PHONY: $(NAME)
$(NAME): $(OUT)

.PHONY: $(NAME)-package
$(NAME)-package: $(NAME)
	gox \
		$(LDFLAGS) \
		-os="$(XC_OS)" \
		-arch="$(XC_ARCH)" \
		-output="$(OUT_DIR)/$(NAME)_{{.OS}}_{{.Arch}}" \
		$(1)

$(eval GENERATED_BINS += $(OUT))
$(eval PACKAGES += $(NAME)-package)

endef

$(foreach src,$(wildcard ./cmd/*),$(eval $(call cmd-tmpl,$(src))))

.DEFAULT_GOAL := all

.PHONY: all
all: $(GENERATED_BINS)

.PHONY: packages
packages: $(PACKAGES)
```

#### ldflags

たとえば [Skaffold はバージョン情報とかも ldflags から読み込んでいる](https://github.com/GoogleContainerTools/skaffold/blob/v0.19.0/Makefile#L42-L48)が，自分はバグレポート受け取るときに便利な最小限のみ ldflags 経由で注入して，バージョン情報などはコード中にハードコードするようにしている．これは go が「`go get` だけでツールをビルド & インストールできる」文化で，ユーザがちゃんと brew や GitHub Release からアプリを落としてくれるとは限らないため．


```make
REVISION ?= $(shell git describe --always)
BUILD_DATE ?= $(shell date +'%Y-%m-%dT%H:%M:%SZ')
LDFLAGS := -ldflags "-X main.revision=$(REVISION) -X main.buildDate=$(BUILD_DATE)"
```

### Release


さっきしれっと出てきたけど，[gox](https://github.com/mitchellh/gox) でクロスコンパイルしている．この生成物を CI から GitHub Release に投げ込む．


```yaml
# .travis.yml
# 自分は Travis CI を使うことが多いけど，いまどきの CI as service なら何でもいいと思う

language: go

go: '1.11'

env:
  global:
  - FILE_TO_DEPLOY="dist/*"

  # GITHUB_TOKEN
  - secure: "..."

jobs:
  include:
  # snip.

  - stage: deploy
    install: make setup
    script: make packages -j4
    deploy:
    - provider: releases
      skip_cleanup: true
      api_key: $GITHUB_TOKEN
      file_glob: true
      file: $FILE_TO_DEPLOY
      on:
        tags: true
    if: type != 'pull_request'
```



### lint / reviewdog

最低限のコードの品質保証とコードレビューの負担軽減用に，いくつかの linter を [reviewdog](https://github.com/haya14busa/reviewdog) を噛ませて使っている．たとえば [grapi の `.reviewdog.yml`](https://github.com/izumin5210/grapi/blob/v0.3.0/.reviewdog.yml) では golint, govet, [errcheck](https://github.com/kisielk/errcheck), [wraperr](https://github.com/srvc/wraperr), [megacheck](https://github.com/dominikh/go-tools/tree/master/cmd/megacheck), [unparam](https://github.com/mvdan/unparam) を有効にしている．

あとは `Makefile` に lint 用のタスクを追加して，CI （pull-req）でチェックしている．

```make
.PHONY: lint
lint:
ifdef CI
	gex reviewdog -reporter=github-pr-review
else
	gex reviewdog -diff="git diff master"
endif
```

```yaml
# snip.

env:
  global:
  # snip.

  - REVIEWDOG_GITHUB_API_TOKEN=$GITHUB_TOKEN

jobs:
  include:
  - name: lint
    install: make setup
    script: make lint
    if: type = 'pull_request'

  # snip.
```

↓こんな感じになる．

![pull-request review by reviewdog](https://qiita-image-store.s3.amazonaws.com/0/14845/e332ca35-fdfe-fcfe-fe18-cf30d8a5c54f.png)


reviewdog を使うことで CI を fail させずに lint の指摘を残せる．なので， golint のコメント関係や errcheck の絶対問題ない系のエラーハンドリングなどをスルーできるようになる．

また，lint ツールやコード生成ツールなどは [**gex**](https://github.com/izumin5210/gex) という tool 管理ツールで管理している．開発者や CI 環境によって利用するツールのバージョンに差異が発生するのを防ぐためである．gex については以前「[gex で Go プロジェクトの開発用ツールの依存を管理する - Qiita](https://qiita.com/izumin5210/items/59a29371809e36d64e04)」という記事で紹介したので，そちらも参考にしてほしい．

clone 直後や CI で便利なように，setup task を `Makefile` に用意している．

```make
.PHONY: setup
setup:
ifdef CI
	curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
endif
	dep ensure -v -vendor-only
	@go get github.com/izumin5210/gex/cmd/gex
	gex --build --verbose
```


## boilerplate generator & utility package 

と，ここまでで「いつも書いてる boilerplate」を紹介した．紹介してないものもいくつかあるので，実際にプロジェクト新規作成時にはもっとたくさん書いている（プロジェクトの性質によって取捨選択はするが）．

流石に自分でもこれを毎回書いてるのはアホらしくなってたのでプロッジェクトジェネレータとライブラリを作った．

https://github.com/izumin5210/clig

こんな感じでプロジェクトが生成されたり

```shell-session
$ clig init your-app-name

$ cd your-app-name
$ tree -I 'bin|vendor'
.
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── cmd
│   └── awesomecli
│       └── main.go
├── pkg
│   └── awesomecli
│       ├── cmd
│       │   └── cmd.go
│       ├── config.go
│       └── context.go
└── tools.go
```

[よく書くコードがまとめられたパッケージ](https://godoc.org/github.com/izumin5210/clig/pkg/cli)があったりする．
また，clig 自身も clig が吐くものとほぼ同じ構成をとっている．

新しく CLI ツールを作ることがあれば参考にしてもらえるといいかもしれない．