7/28で27歳になりました。凪のような暮らしと、荒波のように手を動かし続ける暮らしを交互にやっていて、交互浴ですね。本当にそうか?
最近は、品質・手腕、システムに対する根源的な感情あたりについて考えることが増えたように思います。仕事って感じがしてきているんでしょうか。
相変わらず京都にいて、たまに東京にいるので、飲みに行きましょう。
とりあえず欲しい本のウィッシュリストを貼っておきます。ほかにもオススメの本がありましたらウィッシュリスト外から送ってください。
7/28で27歳になりました。凪のような暮らしと、荒波のように手を動かし続ける暮らしを交互にやっていて、交互浴ですね。本当にそうか?
最近は、品質・手腕、システムに対する根源的な感情あたりについて考えることが増えたように思います。仕事って感じがしてきているんでしょうか。
相変わらず京都にいて、たまに東京にいるので、飲みに行きましょう。
とりあえず欲しい本のウィッシュリストを貼っておきます。ほかにもオススメの本がありましたらウィッシュリスト外から送ってください。
参加しました。Goのコミュニティで発表するのは初めてだったと思います。
発表資料はこちらです。LTと銘打ってはいたけど気楽な感じでやっていました。
以下のブログ記事もあわせてご覧ください。
休憩時間に、登壇した人がテーブルに移動して雑談・質問しに行けるという構造がセッティングされていて、初めて見る形式だったけどなかなかおもしろいなと思いました。人前でサッと手を上げて質問するのはハードルが高いけど聞いてみたいことがある、という人でもこれなら質問しに行けるのかも?
mockgenのコード生成がなぜ遅いのか、他のアプローチだとどうだったのか、などの議論をすることができてよかったです。学会みたいな感じでした。
川やってます #kyotogo pic.twitter.com/NLFtJqeKWP
— うたがわきき (@utgwkk) 2023年7月14日
マネーフォワードさんのオフィスからなめらかに鴨川に移動できて便利でした。
おいしすぎて無限に食べてしまうので危険。
移行しました。といっても元々ペライチのページだし、とくに自明じゃないことはやっていないと思います。
ライブラリが古びていたのでまずはバージョンを上げました。メジャーバージョンアップ上等で、ぱっと見で明らかに壊れていないならいいでしょう、という方針で進めました。VSCodeのformat on saveにもprettierを使っているはずですが、format on saveと prettier --write
でコードの整形結果が違う*1のを目撃したけど、見なかったことにします。
Node.jsのバージョンを .node-version
ファイルで16にpinしていたら古いよ、と言われてNetlifyでのビルドに失敗したので .node-version
ファイルを消したらNode.js 12 (!?) が使われてしまいました。ビルド時に NODE_VERSION
環境変数をセットすることで解決しました。なんかデフォルトでいい感じのLTSバージョンが使われてほしいけどこういう感じなのか?
target: "serverless"
は使えない、と言われたので消したけどふつうに動いていそうです。
ライブラリが最新になったので移行をやっていきます。とはいっても公式ドキュメントのガイドに従っていけばおおむね移行できます。最初はこの移行ガイドも見ていなくて、雰囲気でファイルを用意していったけどなんとかなりました。
next.config.jsで output: "export"
を指定すると手元環境向けのリダイレクト設定を redirects
フィールドに書けないのでどうしたものか、と思っていたのですがビルドフェーズを見ることで手元環境向けの設定とビルド時の設定を分けて書けるようでした。リダイレクトはNetlify側の設定でやっているので手元で動けばじゅうぶんです。
CSSをimportするのもふつうに動いていそうです。
pageコンポーネントが非同期関数になっていて、おもむろにfetchできるのがかっこいいですね。
*1:format on saveだと末尾カンマが入らない
表題のようなツールを作りました。go install
コマンドでただちにインストールできます。
$ go install github.com/utgwkk/bulkmockgen/cmd/bulkmockgen@latest
従来の //go:generate
コメントから移行するツールもあります。
$ go install github.com/utgwkk/bulkmockgen/cmd/mockgen-to-bulkmockgen@latest
Goでinterface定義のモックを生成するツールとしては、mockgen (gomock) がよく知られています。最近 github.com/golang/mock のほうがarchiveされてforkされましたね。
mockgen (gomock) については以下の記事が詳しいので、そちらを参照してみてください。
Goでgomockを使った開発をすることを考えると、mockgenでモックのコードを生成し、テストではモックを使ってアサーションを記述する、というのがよくある開発フローだと思います。たとえば以下のようにinterface定義を書いたのち go generate
コマンドでモックを生成できるように //go:generate
コメントを書くでしょう。
package interfaces import "context" //go:generate mockgen -package mock_interfaces -destination mock_interfaces/iface1.go . Iface1 type Iface1 interface { Meth1(ctx context.Context, arg int) error }
ところで、開発が進むにつれてinterface定義の数が多くなり、//go:generate
コマンドの数も多くなると思います。こうなってくるとよくある問題として、mockgenのコード生成にかかる時間がどんどん長くなります。ひどい場合は go generate ./...
を実行している間に長めに休憩できるかもしれません。
mockgenのコード生成に時間がかかる理由としては、大きく2つのことが挙げられるでしょう。
//go:generate
コメントに書いたコマンドは逐次実行される前者は go help generate
にも書いてあるように、 go generate
コマンドがそのように設計されているためです。
Within a package, generate processes the source files in a package in file name order, one at a time. Within a source file, generate runs generators in the order they appear in the file, one at a time.
go generate
を並列実行できるようにしたい、というproposalが提出されたことがありますがrejectされています*2。
後者はmockgenのreflect mode特有の事情です。
mockgenにはsource modeとreflect modeという2つの動作モードがあります。mockgenのreflect modeは、大まかには以下のように動作します:
重要なことは、reflect modeではmockgenコマンドを実行するたびにプログラムのビルドが行われている、ということです。つまり、素朴に //go:generate
コマンドを都度書いていくとその数だけプログラムのビルドが実行されるということになります。これによってモックの生成に時間がかかってしまいます。
別の話題として、mockgenを go run
で実行するとmockgenのコードが都度コンパイルされるので余計に時間がかかる、というものがあります。
//go:generate
コマンドを1行で書くと人間に優しくないmockgenコマンドが一度で実行されるようにすればよいのではないか、ということを思いつくでしょう。実際、以下のように複数のinterfaceのモックを一括で生成することもできます。
//go:generate mockgen -destination mock_interfaces/mock_interfaces.go -package mock_interfaces . Iface1,Iface2,Iface3
これでひとまずコード生成にかかる時間は短縮できますが、一方で //go:generate
コメントの末尾がどんどん長くなっていくことが予想されます。開発が佳境になって、どんどんinterfaceを足したいというときに複数人が //go:generate
コメントを編集するとコンフリクトの温床となってしまうでしょう。なんとかコンフリクトを解消したと思ったらモックが生成されなくなってしまっていた、という事故が起こりえます。
//go:generate
コメントを複数行にわたって書けるようにする、というproposalが提出されたことがありましたが取り下げられています*7。ということで、2023/7/8 時点では //go:generate
コメントの仕組みに乗った上でmockgenだけを使ったまま高速化するにはコメントを1行で書ききらないといけないようです。
先述したような課題感をなんとかするためにbulkmockgenを作りました。以下のように、モックしたいinterfaceをまとめたスライスを定義しつつbulkmockgenを実行するような //go:generate
コメントを書いたファイルを用意します。
package interfaces //go:generate go run github.com/utgwkk/bulkmockgen/cmd/bulkmockgen MockInterfaces -- -package mock_interfaces -out ./mock_interfaces/mock_interfaces.go var MockInterfaces = []any{ new(Iface1), new(Iface2), new(Iface3), }
この状態で go generate
すると ./mock_interfaces/mock_interfaces.go ファイルに MockInterfaces
に列挙されたinterface (Iface1, Iface2, Iface3) に対応するモックが生成されます。
内部的には、MockInterfaces
に列挙されたinterface名をカンマで結合して、mockgenコマンドを一度だけ実行しています。こうすることによって//go:generate
コマンドを1行で書いたのと同等の効果を得ることができています。--
以降のコマンドライン引数はそのままmockgenに渡ります。
モックしたいinterfaceが増えたときは MockInterfaces
に足せばよいです。1行に1つのinterface名を書けばよいので差分が人間にも優しく、コンフリクトの解消も比較的やりやすいのではないかと思います。
さて、もう既にbulkmockgenを使ってみたくなっていると思いますが、一方で既存のコードには大量の //go:generate
コメントがあることでしょう。これらのコメントを取り除きつつモック対象を列挙していく、というのはなかなかに骨の折れる作業かもしれません。
ということで移行ツールも用意してあり、こちらも今すぐにインストールしてもらえます。
$ go install github.com/utgwkk/bulkmockgen/cmd/mockgen-to-bulkmockgen@latest
以下のように移行したいファイルがあるディレクトリを指定して実行することで移行できます。
$ mockgen-to-bulkmockgen -in_dir ./benchmark/interfaces -out ./benchmark/interfaces/0_bulkmockgen.go
mockgenの //go:generate
コメントを発見して、モックの生成先packageごとに別の変数にinterface一覧を格納して、bulkmockgenでコードを生成する //go:generate
コメントを書いたファイルを生成してくれます。便利ですね。移行ツールでコードを一括で書き換えた上で、コマンドライン引数をちょっと調整してあげればすぐに使えるのではないでしょうか。
ベンチマークとして、20個のメソッドを持つinterfaceを50個定義し、モックを1つずつ生成する場合とまとめて生成する場合とを比較します。
bulkmockgenのディレクトリにベンチマーク用のファイルを用意してある*8ので、これを使いましょう。go generate ./benchmark/interfaces/...
にかかる時間を計測します。
まずは元のコードのまま、interface定義ごとに //go:generate
コメントを書いて go generate
を実行してみます。
% time go generate ./benchmark/interfaces/... go generate ./benchmark/interfaces/... 22.95s user 10.13s system 119% cpu 27.643 total
50個のinterfaceのモックの生成に20秒ほどかかっています。
bulkmockgenでモックを生成するようにコードを修正した上で go generate
コマンドを実行してみましょう。
% time go generate ./benchmark/interfaces/... go generate ./benchmark/interfaces/... 1.62s user 0.48s system 151% cpu 1.380 total
50個のinterfaceのモックの生成がわずか数秒で終わりました。よかったですね。
2023/7/9 時点では、bulkmockgenには以下のような制約があります。
後者について、具体的には以下のようなコマンドに対応するモック生成ができません。
2023/7/29 14:37 追記: generator: interface mocking for external packages by utgwkk · Pull Request #4 · utgwkk/bulkmockgen · GitHub を取り込んだので対応しました。移行ツールについては未対応です。
$ mockgen database/sql/driver Conn,Driver
ひとまず自分が関わっているプロジェクトでのユースケースを網羅できればよい、という方針で実装したのでこのようになっています。真にmockgenを代替したいのであれば、いずれは解決する必要がありそうですね。
mockgenのコード生成を高速化する試みとして、専用のツールで並列にコードを生成するアプローチのgomockhandlerがあります。source modeや依存ライブラリのinterfaceのモックにも対応しており、またコードが最新になっているか確認する機能があることなどからもgomockhandlerのほうが機能は揃っていると思います。
gomockhandlerがあるのにも関わらずbulkmockgenを作ったのは、もっと素朴な方法でもコード生成を高速化できるのではないかと考えたことや、go generate
コマンドの仕組みから逸脱せずにコード生成を高速化できないかと思ったためです (そして実際に高速化できました)。ほかにも、1プロジェクト内でレイヤードアーキテクチャ的に実装する際のモックを用意するのにひとまず特化して作れるのではないか、というのを検証したかったのもありました。
強いてgomockhandlerに対する優位性を挙げるとするならば、-use_go_run
コマンドライン引数を使って go run
形式でmockgenを実行できることや、モックの追加・削除にCLIを使わなくてもよい点などがあるでしょうか。
素朴にはMakefileやシェルスクリプトで並列に生成することもかもしれません。が、筆者の環境ではMakefileを使って並列にmockgenコマンドを叩いてモックを生成しようとしたところ、途中でビルドエラーが発生してうまくモックを生成できませんでした。並列にモックを生成しているという点ではgomockhandlerも同様の問題に当たったと思いますが、どうやって解決したのでしょうか? 今度コードを読んでみます。
MakefileやシェルスクリプトではなくGoのコード中にモック対象のinterfaceを列挙することで、renameに強いという効能が得られています。あとは go generate
の仕組みから逸脱していないので、従来の暮らしからのジャンプが比較的小さいと思います。
mockgenコマンドを一度だけ実行すればよいようにラップすることで、モックの生成にかかる時間を大幅に短縮できました。モックを使った堅牢な開発と素早い実装を両立するのに役立てられればと思います。
READMEがぜんぜん書けていないので誰か助けてください。
*2:https://github.com/golang/go/issues/20520
*3:https://github.com/uber/mock/blob/dac455047760bb7061f57f42615cacfa1fac75c1/mockgen/reflect.go#L50
*4:https://github.com/uber/mock/blob/dac455047760bb7061f57f42615cacfa1fac75c1/mockgen/reflect.go#L152
*5:https://github.com/uber/mock/blob/dac455047760bb7061f57f42615cacfa1fac75c1/mockgen/reflect.go#L165-L169
*6:https://github.com/uber/mock/blob/dac455047760bb7061f57f42615cacfa1fac75c1/mockgen/mockgen.go#L160
*7:https://github.com/golang/go/issues/46050
*8:https://github.com/utgwkk/bulkmockgen/tree/main/benchmark/interfaces