Skip to main content

10 posts tagged with "golang"

View All Tags

· 3 min read
Shunsuke Suzuki

作ったのは 2ヶ月くらい前の話ですが、 Go の command の timeout を実装するためのライブラリを作ったので紹介します。

https://github.com/suzuki-shunsuke/go-timeout

基本的には https://github.com/Songmu/timeout をオススメしますが、これだと上手くいかないパターンがあったので自作しました。

Go の command の timeout に関しては https://junkyard.song.mu/slides/gocon2019-spring/#24 がとても参考になります。

上記のスライドでは

  • 標準ライブラリの exec.CommandContext でも停止できるが、 SIGKILL で強制的に停止することになる
    • 子プロセスが停止しない
  • 公式見解 では、SIGKILL 以外は標準ライブラリではサポートしない。サードパーティでやればよい
  • Songmu/timeout 使えば SIGKILL 以外でより安全に停止できる

ということが丁寧に説明されています。

自分は cmdx という task runner を開発していてその中で task の実行時に timeout を設定出来るようにしました。 当初 Songmu/timeout を使って実装したのですが、問題があることに気づきました。 それは、 command の中で fzf を使うと、上手く動かないというものでした。

正直この辺の挙動はちゃんと理解できていないのですが、 調べてみると Songmu/timeout だと syscall.SysProcAttr の Setpgid を true に設定していて、そうすると fzf が上手く動かないようでした。

https://junkyard.song.mu/slides/gocon2019-spring/#48

https://junkyard.song.mu/slides/gocon2019-spring/#45

には timeout の実装方式として

  • GNU timeout の場合
  • Songmu timeout の場合

の 2 通り書いてありますが、 suzuki-shunsuke/go-timeout では GNU timeout のパターンで実装しています。

https://junkyard.song.mu/slides/gocon2019-spring/#46

に書いてあるとおり、少々乱暴な気もしますが、 cmdx で使う分には特に問題ない気がします。

· 7 min read
Shunsuke Suzuki

今更ながら Golang での時刻の扱い方について改めて整理してみました。

まとめ

  • time.Local は明示的に設定する(基本UTC)
  • DB などには 基本UTC で永続化する
  • 出力時に必要になったらタイムゾーンを変更する
    • location は出力時に問題になるので出力時に location を明示的に指定する
    • 逆に言うと出力時以外は問題にならないので無理に location を UTC にしなくても良いかもしれない
    • サードパーティ(ex. ORM) に time.Time を渡す場合は location に注意が必要
  • 文字列として時刻の入力を受け付ける場合は location を明示的にセットする
  • サードパーティが time.Local に依存する場合、 time.Local を明示的に UTC にしたりする必要があるかもしれない
  • アプリケーションで利用する location が分かっている場合、location を取得するヘルパー関数を定義する
  • time.LoadLocation は環境依存なので予め location が分かっているなら使わないほうがよい
  • 文字列を time.Time に変換する場合、time.ParseInLocation で Location を指定して time.Time に変換後、time.Time.UTC() で UTC に変換する
  • time.Time を文字列に変換する場合、time.In で location を変換後、time.Time.Format で文字列に変換する

グローバルな location

https://golang.org/pkg/time/#Location

Local represents the system's local time zone.

location を設定する

https://crieit.net/posts/Go-time-LoadLocation に書いてあるとおり、 time.LoadLocation を下手に呼び出すと環境によっては unknown time zone エラーが起こるため 次のように time.FixedZone で Location を生成します。

jp := time.FixedZone("Asia/Tokyo", 9*60*60)

FixedZone という関数名が紛らわしい気もしますが、新しい Location を生成しているだけで副作用はありません。

ちなみに time.FixedZone に渡す文字列は "foo" みたいな適当な文字列でも動くようです。

https://golang.org/pkg/time/#FixedZone

https://github.com/golang/go/blob/9e277f7d554455e16ba3762541c53e9bfc1d8188/src/time/zoneinfo.go#L263-L308

アプリケーションで利用する location が決まっている場合、次のように location を返すヘルパー関数を用意すると良さそうです。

package location

import (
"time"
)

var (
jp *time.Location
)

func init() {
jp = time.FixedZone("Asia/Tokyo", 9*60*60)
}

func JP() *time.Location {
return jp
}

厳密に UTC な Location を取得する

厳密に言うと、time.UTC は変更可能なので UTC だとは限りません。 そのため、本来 time.UTC はゲッター関数であるべきだったんじゃないかなという気もします。

厳密に UTC な Location を取得するにはヘルパー関数を書くと良さそうです。

package location

import (
"time"
)

var (
utc *time.Location
)

func init() {
utc = time.FixedZone("UTC", 0)
}

func UTC() *time.Location {
return utc
}

動作環境に依存しないコードにするために

動作環境によって time.Local の値が違うことで結果が変わってしまう場合があります。 それを防ぐために、プログラムの最初に time.Local を UTC にするという手もありそうです。

time.Local = location.UTC()

ただし、それでもグローバル変数である以上、行儀の悪いサードパーティのライブラリによって変更されるかもしれませんし、 必要な箇所で location を明示的に指定してグローバル変数に依存しないようなコードを書くことを心がけたほうが良い気もします。

文字列を time.Time に変換する

ParseInLocation is like Parse but differs in two important ways. First, in the absence of time zone information, Parse interprets a time as UTC; ParseInLocation interprets the time as in the given location. Second, when given a zone offset or abbreviation, Parse tries to match it against the Local location; ParseInLocation uses the given location.

ParseInLocation と Parse の違い

  • 文字列に location の情報がない場合、 Parse は UTC として扱う
  • zone offset が指定された場合、 Parse は Local location からの offset として扱う
    • ParseInLocation で明示的に Location を指定したほうが良さそう

予め location がわかっている場合 time.ParseInLocation で location を指定して time.Time に変換した後 time.Time.In で UTC にするのが良さそうです。

t, err := time.ParseInLocation("2006-01-02T15:04:05", "2019-08-13T21:30:00", jp)
if err != nil {
return err
}
t = t.UTC()

time.Time の Location を変更する

https://golang.org/pkg/time/#Time.In

In returns a copy of t representing the same time instant, but with the copy's location information set to loc for display purposes.

time.Time.In は time.Time の Location だけ変更したコピーを返します。

package main

import (
"fmt"
"time"
)

func main() {
time.Local = location.UTC()
t := time.Now()
fmt.Println(t) // 2019-08-14 12:08:44.150725 +0000 UTC m=+0.000212031
t2 := t.In(location.JP())
fmt.Println(t2) // 2019-08-14 21:08:44.150725 +0900 Asia/Tokyo
}

time.Time を文字列に変換する

https://golang.org/pkg/time/#Time.Format

time.Time.In で location を変更した後 time.Time.Format で文字列に変換するのが良さそうです。

time.Now の location

Location は time.Local になります。

https://golang.org/pkg/time/#Now

他のパッケージの location の扱い

log

log パッケージで出力される時刻のフォーマットと location は log.SetFlags によってある程度変更できます。

デフォルトは 日時を time.Local で出力します。 log.LUTC をセットすることで UTC になります。

log.SetFlags(log.Flags() | log.LUTC)

logrus

logrus のログの時刻の location も time.Local なようです。

robfig/cron

https://github.com/robfig/cron

All interpretation and scheduling is done in the machine's local time zone (as provided by the Go time package (http://www.golang.org/pkg/time).

time.Local なようです。

go-sql-driver/misql

gorm

https://github.com/jinzhu/gorm/wiki/How-To-Do-Time

· 4 min read
Shunsuke Suzuki

2019-07-17 追記

プロジェクト名が変わりました

https://github.com/suzuki-shunsuke/flute/issues/20


Go の HTTP client のテストフレームワークを作ったので紹介します。

https://github.com/suzuki-shunsuke/flute

執筆時点のバージョンは v0.6.0 です。

  • リクエストパラメータのテスト
  • HTTP サーバのモッキング

を目的としています。

比較的実践的なサンプルとして、ユーザーを作成する簡単な API client とそのテストを書いたので参考にしてください。

元々自分はこの目的のために h2non/gock を使っていました。 ただ、 gock だとリクエストがマッチしなかったときに、なぜマッチしないのかがわからず、調査に困るという問題がありました。

そこで flute では request に対し、matcher と tester という概念を導入し、 matcher でマッチしたリクエストを tester でテストするというふうにしました。 テストでは内部で stretchr/testify の assert を使っており、テストに失敗したときになぜ失敗したのかが分かりやすく出力されるようになっています。

例えば以下の例は、リクエストの Authorization header にトークンがセットされていなかった場合のエラーメッセージです。

=== RUN   TestClient_CreateUser
--- FAIL: TestClient_CreateUser (0.00s)
tester.go:168:
Error Trace: tester.go:168
tester.go:32
transport.go:25
client.go:250
client.go:174
client.go:641
client.go:509
create_user.go:45
create_user_test.go:56
Error: Not equal:
expected: []string{"token XXXXX"}
actual : []string{"token "}

Diff:
--- Expected
+++ Actual
@@ -1,3 +1,3 @@
([]string) (len=1) {
- (string) (len=11) "token XXXXX"
+ (string) (len=6) "token "
}
Test: TestClient_CreateUser
Messages: the request header "Authorization" should match
service: http://example.com
request name: create a user

また、当たり前かもしれませんが、モックとしてレスポンスも返します。

マッチングやテストで使える項目としては

  • リクエストパス (ex. "/users")
  • method (ex. "GET", "POST")
  • クエリパラメータ(パラメータの有無、値)
  • ヘッダー(ヘッダーの有無、値)
  • リクエストボディ
    • 文字列完全一致
    • JSONとしての等価性
  • ユーザー定義のカスタム関数

などがあります。

詳細は コード中にコメントを入れているので godoc を読んでください。

技術的には *http.Client の Transport に *flute.Transport を設定することで HTTP サーバのモッキングをしています。

API のデザイン面で考慮したこととしては、 グローバル変数である http.DefaultClient の変更をライブラリ側でやらないことです。 あくまで http.RoundTripper の実装を提供するだけで、それを http.DefaultClient に設定する場合、それのコントロールはユーザーに任せています。

  • ライブラリでグローバル変数の変更を隠蔽し、ユーザーが無意識のうちに変更してたりするのは良くない
    • gock では http.DefaultClient を変更しているが、それを理解しないまま使っているユーザーもいるはず
    • グローバル変数の変更には副作用もあるので、ユーザーが理解した上で明示的に行うべきである
    • 明示的に http.DefaultClient = client のようにユーザーに書かせれば、理解しないまま使うことはないはず
  • ライブラリの外からも変更できるグローバル変数をライブラリで完全に管理するのは不可能なので、ユーザーに任せる

以上、簡単ですが自作の OSS flute の紹介でした。

· 2 min read
Shunsuke Suzuki

https://github.com/suzuki-shunsuke/go-jsoneq

2つの値がJSONとして等しいか比較するGoライブラリを開発したので紹介します。

「2つの値がJSONとして等しい」とは、2つの値をそれぞれJSON文字列に変換したら、2つが表現するデータがおなじになるという意味です。

struct {
Foo string `json:"foo"`
}{
Foo: "bar",
}

map[string]interface{}{"foo": "bar"}

を JSON に変換したらともに

{"foo": "bar"}

になりますね。

json.Marshaler のテストや、 実際の JSON 文字列から構造体を定義したときにちゃんと定義できているかチェックするのに使えると思います。

jsoneq.Equal でやっていることは単純です。

  1. json.Marshal で []byte に変換
  2. json.Unmarshal で []byte を map, array と primitive な型からなるオブジェクト(?)に変換
  3. reflect.DeepEqual で比較

引数が []byte の場合は 1 は飛ばします。

GoDoc やサンプルを見れば使い方は簡単にわかると思います。

以上、簡単ですが、自作ライブラリの紹介でした。

· 2 min read
Shunsuke Suzuki

Golang の設定管理のライブラリといえば viper が有名ですが、 confita も良さそうだったので紹介したいと思います。

confita の機能としては以下のようなものがあります。

  • 構造体に設定をマッピング
  • flag や環境変数、設定ファイルに対応
  • 複数の設定ファイルに対応

構造体に設定をマッピングすることで、https://github.com/go-playground/validator のようなライブラリを使って設定のバリデーションが出来ます。

また viper は v1.3.1 の時点で複数の設定ファイルを扱いにくいです。

Viper can search multiple paths, but currently a single Viper instance only supports a single configuration file.

k8s で ConfigMap と Secret を設定ファイルとして扱う場合、複数のファイルを扱えないと不便です。 その点 confita は複数の設定ファイルを問題なく扱えます。

以下はフラグで指定した複数の設定ファイルから設定を読み込む簡単なサンプルです。

import (
"context"

"gopkg.in/go-playground/validator.v9"

"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/heetch/confita/backend/file"
flag "github.com/spf13/pflag"
)

func loadConfig(ctx context.Context) (Config, error) {
cps := flag.StringSliceP("config", "c", nil, "configuration file path")
flag.Parse()
fileBackends := []backend.Backend{}
for _, p := range *cps {
fileBackends = append(fileBackends, file.NewBackend(p))
}

loader := confita.NewLoader(fileBackends...)
cfg := Config{LogLevel: "info"} // default value
if err := loader.Load(ctx, &cfg); err != nil {
return cfg, err
}
validate := validator.New()
if err := validate.Struct(cfg); err != nil {
return cfg, err
}
return cfg, nil
}

以上、簡単な紹介でした。 viper 以外のライブラリを探している人は試してみてください。

· 3 min read
Shunsuke Suzuki

自分は 2017/8頃(曖昧)からメインで書く言語を Python から Go に変更しました。 Goを書き始めて割と早い段階でGoが一番好きになりました。 そこでなんで Go が好きなのかということを頑張って言語化しようと思います。

若干他の言語と比較する部分もありますが、決して他の言語をディスったり、 他の言語より優れているということが言いたいわけではないのでご了承ください。

  • 依存するものが小さく、バイナリ1つインストールするだけで良い
    • Prometheus の exporter とかインストールするの簡単
    • Docker Imageも最小限になる
  • 静的型付け
    • ビルド出来ている時点で一定の信頼性が担保されている
    • よく知らないコードを読んだり修正するときとかだいぶ有り難い
  • GoDocが素晴らしい
    • 何もしなくてもライブラリのドキュメントが出来上がっている
  • ライブラリの公開が容易
    • GitHubに公開するだけ
    • npm や pypi のようなレジストリがないので楽
  • go test とか go vet, gofmt みたいに標準ツールが揃っている
  • コーディング規約で悩む必要がない
  • lintツールが充実している
    • gometalinter とか使っておけば OK
    • lintできる環境を構築するのにそこまで頑張らなくて良い
  • エラーハンドリングが暗黙的に省略できないので信頼性が高い
    • Goのエラーハンドリング嫌いって人もいるし、v2で改善されるって話も聞くけど、自分はむしろ好き(面倒なのは理解できるけど)
  • 言語仕様がシンプル(客観的な根拠はないし、難しい部分もあるけど、そんな気がする)
    • メタプログラミング使った、魔術的なコードになりにくい
  • interface 使ってコードを疎結合にするのが書いてて気持ちいい
  • 並列処理が書きやすい

· 3 min read
Shunsuke Suzuki

以前 Golang のロギング・エラーハンドリングについて書きました。

それを少し v0.1 から v0.2 に互換性を壊す形でアップデートしようかと思います。 本記事ではその変更点について書きます。

変更点

関数のエラーに情報を付与する責務を関数に割り当てていたものを、呼び出し元に割り当てるようにします。

具体的には元々

func createUser(name string, age int) error {
return errlog.Wrap(checkName(name), logrus.Fields{"age": age}, "failed to create a user")
}

だったものが

func createUser(name string, age int) error {
return errlog.Wrap(checkName(name), nil, "user name is invalid")
}

になります。

変更理由

メタ情報のフィールド名はコンテキストに依存します。 上記の例だとユーザー名というメタ情報のフィールド名は name より user_nameadmin_name, owner_name としたほうが適切かもしれません。それは関数内部では分からず、呼び出し元でないと分かりません。呼び出し元でないとフィールド名の衝突が避けられないこともあるでしょう。

メッセージに関しても同様のことが言えます。 また、元々 v0.1 ではユーザーが定義した関数と

  • 標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数
  • interface の関数やメソッド

を区別し、前者では関数側でエラーに情報を付与させる一方、後者では呼び出し元で情報を付与させるというふうにしていました。

v0.2 では両者を区別せず、どちらの場合でも呼び出し元に付与させるというふうにすることでよりルールがシンプルになります。

· 11 min read
Shunsuke Suzuki

2018-12-30 追記

この記事を元にドキュメントを書いてみました。

https://github.com/suzuki-shunsuke/go-error-handling-logging-practice

追記ここまで


Go でエラーハンドリングとロギングをしてきて自分の中で固まりつつあるプラクティスを明文化します。 明文化することで以下のことを目指します。

  • 迷いをなくす
  • コードの一貫性を保つ
  • コーディング規約とすることでレビューの品質を上げる(自動化は出来ないけど)
  • コードの品質を上げる(コードがゴチャつかなくなる)
  • 適切にエラーをロギングする(必要十分な情報をログとして残す)

またエラーハンドリングとロギングのためのライブラリを自作しているのでそれも紹介します。

https://github.com/suzuki-shunsuke/go-errlog

ロギングに関する関連記事

この記事を書く前に軽くググってみただけでちゃんと読んでないのですが、 興味のある人は読んでみてください。

ログレベルは分ける

ログレベルでwarningとかいらないという意見もありますが、自分は必要だと思っています。 自分は以下のログレベルを使い分けます。

  • debug: あまり使わない。調査目的で一時的に埋め込むログ。調査が終わったら出力しないようにする。一時的でないものはinfoにする
  • info: エラーでないログ。イベント、処理の開始時や終了を記録するのに使うことが多い
  • warn: 4xx系のエラー。それが起こっただけではアラートを飛ばさないが、数が通常時より多い場合はバグかUIに問題があってユーザーが間違えやすくなっている可能性があるのでアラートを飛ばす
  • error: 5xx系のエラー。アラートを飛ばす(閾値は調整)
  • fatal: 処理継続が不可能な致命的なエラー。システムを止める

書いてから思いましたが、これに関しては標準的な使い分けのルールがありそうですね(要調査)。。

logrus を使ってログを構造化する

前提としてwebシステムやバッチシステムなどを想定しています。CLIツールならば話は変わるでしょう。 JSONフォーマットで出力してfluentdでElasticsearchにフォワードするのが個人的によくあるパターンです。

go-errlogもlogrusの使用を前提としています。

ロギングのライブラリは他にも色々あるので、logrusで満足できない人は以下から探してみるとよいでしょう。

https://github.com/avelino/awesome-go#logging

エラーログは中央集権的に main に近い所で出力する

エラーログをどこで出力するかですが、原則中央集権的に main に近い所で出力します。 因みに中央集権的という表現は echo の centralized error handling からもじっています。

https://echo.labstack.com/guide/error-handling

error が発生してもすぐログを吐くのではなく、error を関数の戻り値として返し、ロギングする責務を親に委譲します。 Goでは以下のようなイディオムがよく見られますね。

if err != nil {
return err
}

ロギングに必要な情報を戻り値のerrorに含める

上記のコードで問題なのは、エラーに関する情報が欠損することがあることです。

これに関しては以下の記事が参考になります。

https://deeeet.com/writing/2016/04/25/go-pkg-errors/

エラーに関する情報には2種類あると個人的に考えていて「メッセージ」と「メタ情報」なんて風に脳内で呼んでたりします。

  • メッセージ: エラーの原因を示すhuman readable なテキスト(pkg/errorsはこれに対応している)
    • リストになる
  • メタ情報: エラーに関する構造化されたデータ
    • ハッシュになる

例えば foo というユーザー名が既に使われていてユーザーの作成に失敗した場合

  • メッセージ
    • username is already used
    • invalid username
    • failed to create a user
  • メタ情報
    • username: foo

と言った感じになります。 メッセージにメタ情報を含めて "foo" is invalid username といった風にも出来ますが、そうすると検索・集計しづらかったり、メッセージの生成に一手間かかったりするのでメッセージにはメタ情報を含めません。

pkg/errors だとメタ情報には対応できないので自分でライブラリを作りました。

https://github.com/suzuki-shunsuke/go-errlog

こんな感じになります。

return errlog.Wrap(err, logrus.Fields{"username": "foo"}, "failed to create a user")

error に含める情報の責務

上記のように error に情報を含める場合、どこまで含めるかというのが問題になります。 ここでプラクティスとして、 関数がerrorを返す場合、その関数がもっている情報は全て含める責務があり、 逆に子関数から返ってきたerrorには子関数に渡っている情報が含まれているので呼び出し元で付与する必要はないというふうにしています。

func createUser(name string, age int) error {
if err := checkName(name); err != nil {
return errlog.Wrap(err, logrus.Fields{"age": age}, "failed to create a user")
}
}

つまり上のコードでは子関数に渡っているメタ情報nameや、invalid username のようなメッセージを errlog.Wrap に渡す必要はありません。 上記の例だとエラーに関係ない age も渡す必要はないのではないかとも考えられますが、原則ログに残すこととします。

ただし、子関数が標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数であれば話は別です。 それらがどのようなエラーを返すかは保証がありません。

if f, err := os.Open(filename); err != nil {
return errlog.Wrap(err, logrus.Fields{"filename": filename}, "failed to open a file", "failed to create a user")
}

上記の例だと、os.Openに渡したメタ情報 filename や os.Openに失敗したことを示す failed to open a file といったメッセージもerrlog.Wrapに渡しています。

errlog.Wrap は複数のメッセージを渡せるようになっています。 メッセージの順番は左からイベントが発生した順になるようにします。 上記の例だと「ファイルのオープンに失敗」した結果、「ユーザの作成に失敗」するという順序になります。

エラーのロギングはシンプルに

go-errlogではシンプルにロギングを記述できます。

logger := errlog.NewLogger(nil)
// err != nil なら logging する
// err がメタ情報を持ってたら logrusで構造化してロギングする
// メッセージも pkg/errors のように一つのテキストに連結してロギング
logger.Fatal(createUser("foo", 10))

その他 go-errlog の機能

メタ情報やメッセージによって条件分岐したり出来るようにヘルパー関数を幾つか提供しています。

  • CheckField
  • HasField
  • HasMsg

詳細はGoDocやソースコードを見てください。

最後に

色々書いてしまいましたが、一番言いたかったことは

関数がerrorを返す場合、その関数がもっている情報は全て含める責務があり、 逆に子関数から返ってきたerrorには子関数に渡っている情報が含まれているので呼び出し元で付与する必要はないというふうにしています。

ただし、子関数が標準関数やサードパーティのライブラリなど、プロジェクト外部で定義された関数であれば話は別です。

の部分です。この辺は元々自分の中でルールが決まってなくてずっとモヤモヤしてて、 コードを書くたびにぶれてたのですが、「こうすればいけるんじゃないか」と思いつき、その実装を補助するライブラリを開発し、 実践したところ今の所そこそこうまく行っています。 ただまだ日が浅いので少しずつブラッシュアップされていく部分もあると思いますが、 その場合でも「なんとなく」ではなく、可能な限り明文化していくことで、迷いをなくし、コードとログの品質を上げていきたいと思います。

· 9 min read
Shunsuke Suzuki

自作のOSS gomic の紹介をします。

  • なぜわざわざこんなものを作ったのか
  • 生成されたモックの簡単な使い方

を主に説明したいと思います。

まとめ

  • gomic は Goのinterfaceを実装したモックを生成するCLIツール
  • モックを手で書くのが辛すぎた & 既存ツールで満足できなかったため作った
    • 自動生成できるコードは自動生成すべき
  • 設定ファイルで管理するため、interfaceの更新に合わせてmockの更新が容易
  • 生成されるモックはシンプルなAPIのみ提供するので学習コストが低い

gomic とは

gomic は Goのinterfaceを実装したモックを生成するCLIツールです。 これによってモックを使ったテストの作成を効率化します。 単調な作業を自動化し、本来注力すべきことに注力できるようにするためのツールです。

Goで書かれています。

https://github.com/suzuki-shunsuke/gomic/releases からバイナリをダウンロードしてインストールできます。

同様のツールは幾つかあります。

特に gomock は有名ですね。

なぜ作ったのか

上述のように既に同様のツールはありますし、 gomock と minimock は試しました。 しかしあまり満足のいくものではなかったため、自分で作ることにしました。

自分が欲しかったのは学習コストの低いシンプルなAPIです。 interfaceのメソッドを実装した関数をモックに渡すことで 簡単にメソッドの実装を切り替えたいのです。

// Getwd メソッドのモック
mock.SetFuncGetwd(func() (string, error) {
return "/tmp", nil
})

mock.Getwd() // "/tmp", nil

これは非常にシンプルで分かりやすく、柔軟性のあるパターンです(minimockはこのパターンもサポートしています)。

gomock や minimock では

mockSample.EXPECT().Method("hoge").Return(1)

のように 関数のパラメータと戻り値のペアを渡してモックを実装するパターン(何か名前があるのでしょうか?)をサポートしています。 このパターンを gomic はサポートしていません。 このパターンはごく簡単なサンプルでは有効かもしれませんが、実際には使えないことが多いかなと感じています。

また、gomock はそれ以外にも gomock.InOrdergomock#Call.After など、色々便利なAPIを提供していますが、 それらは学習コストを上げてしまう要因になると思います。 gomicはそういったAPIは提供していません。

素のGoで良いのでは(gomicいらなくない)?

上述のように関数を渡すだけの実装なら gomic なんて使わなくても素のGoで良いのではないかという意見もありそうですね。

http://haya14busa.com/golang-how-to-write-mock-of-interface-for-testing/

でも似たようなモッキングの方法がライブラリに依存しないでmockを書くパターンとして紹介されています (似たようなというか、gomicも v0.4.0 までは構造体のフィールドに代入していました)。

Goではライブラリに依存しないで標準ライブラリだけで書くのが良いという思想・意見がよく見られます。 そのため、gomicのようなツールを好まない方がいるのは承知しています。

ただ、自分はこのパターンの実装を手で愚直に書くのは辛いし、生産的ではないのでツールによって自動生成すべきだと思っています。

以下は2つのメソッドのみ持つシンプルなインタフェースとそのモックです。

とてもシンプルな interface とそのモックですが、それでもモックを実装するのはそこそこ面倒です。 メソッド、interfaceの数に比例してどんどん面倒になります。 golintのようなlinterでエラーにならないようにコードコメントを書くのも地味に大変です。

interfaceを更新すればmockも更新しないといけません。

ツールによって自動化すべきです。

モックの使い方

生成されたモックの使い方について軽く説明します。 v0.5.0 時点のものなので古くなっているかもしれません。 最新の使い方は

をご確認ください。

以下のサンプルは v0.5.0のサンプル を元にしています。

まず mock を生成します(以下このモックを生成する関数を"コンストラクタ"と呼びます)。

mock := examples.NewOSMock(nil, nil)

第一引数は testing.T で、通常のテストならテスト関数の引数をそのまま渡せば良いし、そうでなければ nil を渡せば良いと思います。 第二引数は `func(t testing.T, intfName, methodName string)` 型の関数で、interfaceのメソッドの実装がセットされていない場合に呼び出されます。nil を渡すと代わりにgomic.DefaultCallbackNotImplemented が呼び出されます。

mockは interface を実装しています。

次にinterfaceのメソッドを実装した関数をmockにセットします。

mock.SetFuncGetwd(func() (string, error) {
return "/tmp", fmt.Errorf("")
})

mock.Getwd を呼び出すと SetFuncGetwd に渡した関数が呼び出されます。

上記のサンプルのように決まった値を返すだけの fake はよくあるので、以下のように簡単に書けるようにしています。

mock.SetReturnGetwd("/tmp", fmt.Errorf(""))

モックの SetFuncXXX 及び SetReturnXXX はモック自身を返すのでメソッドチェーンが出来るようになっています。

mock := examples.NewOSMock(nil, nil).
SetReturnMkdir(nil).
SetFuncGetwd(func() (string, error) {
return "/tmp", fmt.Errorf("")
})

実装がセットされていない状態でモックのメソッドを呼び出すと コンストラクタの第二引数で渡した関数が呼び出されます。

コンストラクタの第二引数がnilだと gomic.DefaultCallbackNotImplemented が呼びだされます。 gomic.DefaultCallbackNotImplemented は コンストラクタの第一引数が nil だと log.Fatal を、そうでなければ testing.T#Fatal を呼び出し、そこで処理を停止します。

コンストラクタの第二引数で渡した関数で log.Fatal や testing.Fatal によって処理を止めなければ、interfaceのメソッドを実装していない場合、zero value を返す fake になります。

一番簡単なのは gomic.DoNothing を渡すことです。

s, err := mock.Getwd(nil, gomic.DoNothing)

上で説明したことは

https://github.com/suzuki-shunsuke/gomic/blob/v0.5.0/examples/os_mock.go#L27-L67

を見てもらえばわかると思います。

· One min read
Shunsuke Suzuki

自作のOSS go-gencfg を紹介します。 Golang で viper という汎用的な設定管理ライブラリがありますが、 特定のアプリケーション用に viper のラッパーを生成するCLIツールです。

使い方や開発の背景を書こうかと思いましたが、だいたい README に書いてあるので そちらを御覧ください。

https://github.com/suzuki-shunsuke/go-gencfg/blob/master/README.md