go言語のプロジェクトの雛形を作る

go言語を触っておきたかったのでプロジェクトの雛形を作る事にした。
自分は、新しい言語を触るときは予めテストの書き方デバッガの設定モックを注入する方法などを調べてプロジェクトの雛形を作る様にしている。
そんな訳でやっていく🤘

結論だけ見たい方は こちら ⇛ t-kuni/go-cli-app-skeleton

go言語をインストールする

インストール手順に従って進めていく。

特に詰まる点は無かったのでほぼ割愛。
以下の様なHello worldを書いてコンパイルして動くところまで確認した。

package main

import "fmt"

func main() {
	fmt.Printf("hello, world\n")
}

Golandでプロジェクトを作成する

GolandとはJetBrains製のGO用IDEです。
クイックスタートを参考に進めていく。

Golandの実行ボタンを押すだけでビルドから実行までやってくれるっぽい。

ユニットテストのセットアップ

ユニットテストは何かとお世話になるので書き方を調べておく。
このドキュメントを参考にする。
ユニットテストの書き方は以下の通り。

  • ファイル名の末尾に_test.goを付ける。
  • 関数名はTestXxxの様にTestから始める。

プロジェクトを右クリックして「Run」「go test ...」を押下するとテストが実行される

ここでtesting: warning: no tests to runというエラーが発生した。

調べた所、ユニットテストの関数名をtestXxxの様にキャメルケース(小文字から開始)で記述していたためテストケースとして扱われなかったらしい。
ユニットテストの関数名はパスカルケースで書く必要があるとの事(詳しくは次章)

Golangの命名規則

ユニットテストでハマったので命名規則について調べる。

  • 変数名やメソッド名
    • キャメルケース(hogeFuga) or パスカルケース(HogeFuga)で書く
    • キャメルかパスカルかで可視性が異なる。キャメルはprivate、パスカルはpublic
  • ファイル名
    • スネークケース

キャメルかパスカルかで可視性が異なるのは入門者のハマりポイントって感じがしますねー

ユニットテストをデバッグ実行する

どうせユニットテスト書くならそのなかでブレークポイント掛けたりしたいよねという事でデバッグ方法を調べていく。
とりあえず素直にデバッグ実行してみる。

Golandで以下のエラーが発生した。
「ディレクトリのような実行構成ではコンパイルを実行できません」との事でGolandの実行構成がまずい事はなんとなくわかる。

Error running 'go test go-cli-app-skeleton': Cannot run compiling on directory-kind run configurations

GOPATH is emptyという警告が出ていることに気付いた。

GOPATHとは?

ワークスペースの場所を表している。デフォルトでは$HOME/goになっている。
ワークスペースとはGo コードを探す場所を表すらしく、パッケージをビルドしたりインストールしたりする際に使われるとの事。
このワークスペースにはsrcフォルダとbinフォルダが必要で
srcフォルダには各種パッケージとそのソースコードが、
binフォルダには実行可能なバイナリが配置されるとの事

という訳で~/.bashrcexport GOPATH=$HOME/.goを追記した

しかし、これだけでは解決しなかった

JetBrainsのフォーラムを眺めていると「プロジェクトをGOPATH内に配置しないとテストのデバッグ実行はできません」的な事が書かれているのを発見した。
という訳でプロジェクトを~/.go/src/配下に移動した。
これによって、デバッグ実行でき、無事にブレークポイントが効く状態になった。

それにしても特定のフォルダ構造配下に自分のプロジェクトを置かなきゃいけないのはちょっとどうなんだという気持ちになる。
これがgo言語の仕様なのかGolandの仕様なのか入門したばかりの自分には良くわからないが・・・。

DIコンテナを導入する

ユニットテストを書くにあたって副作用を伴う処理はモック化するのだが、そのためにDIコンテナを使って依存性の注入を行う。
sarulabs/diというパッケージがあるのでこれを使ってみる。

Golandでは、import文を追記して該当の行でAlt+Enterを押下するとパッケージをダウンロードできる。

DIコンテナの初期化はこんな感じになる。
di.NewBuilder()でDIコンテナの作成を開始し、
di.Defでサービス名とそれに対応する構造体を定義する。
builder.Build()で定義に従ってDIコンテナを作成する。

func createApp() di.Container {
	builder, _ := di.NewBuilder()

	builder.Add([]di.Def{
		{
			Name:  "quotation-generator",
			Build: func(ctn di.Container) (interface{}, error) {
				return SimpleQuotationGenerator{}, nil
			},
		},
	}...)

	return builder.Build()
}

DIコンテナからサービスを解決するには[DIコンテナ].Get("サービス名")を使う。
末尾の.(QuotationGenerator)はキャストを行っている。

generator := app.Get("quotation-generator").(QuotationGenerator)

モック生成ツールを導入する

DIコンテナは導入できたので、golang/mockというモックを生成するツールを導入する
以下のコマンドで既存のコードからモックを生成できる。

mockgen -source=clock.go -package=main -destination=clock_mock.go

現在時刻を返す自作のインタフェースClockerからモックを生成すると以下の様なコードが生成された
構造体MockClockerを作成して、インタフェースに定義されている関数を生やしているのが分かる。

// 省略

// MockClocker構造体
type MockClocker struct {
	ctrl     *gomock.Controller
	recorder *MockClockerMockRecorder
}

// MockClockerのコンストラクタ
func NewMockClocker(ctrl *gomock.Controller) *MockClocker {
	mock := &MockClocker{ctrl: ctrl}
	mock.recorder = &MockClockerMockRecorder{mock}
	return mock
}

func (m *MockClocker) EXPECT() *MockClockerMockRecorder {
	return m.recorder
}

// 生成元のインタフェースで定義していた関数がモック仕様で実装されている
func (m *MockClocker) Now() time.Time {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Now")
	ret0, _ := ret[0].(time.Time)
	return ret0
}

func (mr *MockClockerMockRecorder) Now() *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClocker)(nil).Now))
}

モックを使用するときはこんな感じ。
defer ctrl.Finish()deferは関数を遅延実行させる仕組みらしい。テスト関数がreturnする時にctrl.Finish()が呼び出されてモックの呼び出し結果が検証されるんだと思う。
NewMockClockerでモックを生成して、[モック].EXPECT().[関数名]().Return("戻り値")で関数が呼び出される事の検証と呼び出された時の戻り値を設定している。

ctrl := gomock.NewController(t)
defer ctrl.Finish()

loc, _ := time.LoadLocation("Asia/Tokyo")
clocker := NewMockClocker(ctrl)
clocker.EXPECT().Now().Return(time.Date(2020, 1, 1, 0, 0, 0, 0, loc))

actual := RandomQuotationGenerator{Clocker: clocker}

.envを使えるようにする

環境毎に処理を切り替えたりするために.envファイルを読める様にする。
joho/godotenvのREADMEにしたがってインストールする。
特にハマりポイントはなかったので割愛。

フォルダ構成をスタンダードに合わせる

Goにはディレクトリ構成のスタンダードがあるらしい。 という記事を参考にフォルダ構成を整えた所、以下の様になった。
cmdフォルダにはエントリポイントとなるソースコードを格納するらしい。app-nameはコマンド名と揃える必要があるとの事。
internalフォルダにはこのプロジェクトでしか使用しないコードを格納する。ほとんどのコードはここに配置されるはず。
pkgフォルダは他のプロジェクトとも共有するコードを格納する。

cmd
└ app-name
  └ main.go
internal
├ clock.go
├ clock_mock.go
├ quotation_generator.go
└ quotation_generator_test.go
pkg
.env

おわりに

最終形はこうなりました。参考までに。

t-kuni/go-cli-app-skeleton

コメントする