これはGo Advent Calendar 2018の14日目の記事となる。
この記事ではGoogleが提供するGoのDependency Injection(DI、依存性の注入)ツールであるWireを使ったDIの概要と、Wireで利用可能なDI関数の戻り値シグネチャのパターンを紹介する。
- github.com/google/wire
- Compile-time Dependency Injection With Go Cloud’s Wire | The Go blog
TL;DR
- WireコマンドはGoogle謹製のGoでDI(依存性の注入)を行なうツール
- https://github.com/google/wire
- 依存の注入・初期化、依存の注入・初期化を繰り返して多階層になるDIコードも自動生成できる
- DIで利用できる関数の戻り値のパターンは4種類(引数は任意)
- func() Object
- func() (Object, error)
- func() (Object, func())
- func() (Object, func(), error)
- Wireを利用すると疎なpkg構成とDIコードの自動生成を享受できる
Wireについて
WireはGoogleがgo-cloudのリポジトリを公開したときに同梱されていたDIツールだ。
2018年12月にWireだけ別のリポジトリに分離され、独立して管理されるようになった。
次節から説明するwire
pkgを使ったDIパーツ・組み合わせの定義に対してgo get
で入手できるwire
コマンドを実行するとDIの初期化コードを自動生成できる。
$ go get github.com/google/wire/cmd/wire
$ wire -h
usage: wire [gen|diff|show|check] [...]
Wireを使ったDIパーツの定義
Wireを使ったDIを利用するにはまずwire.NewSet
を使ってDIのパーツ(Provider)を定義する必要がある。
(コードはDefining Providers | Wire User Guideより抜粋)
package foobarbaz
import (
"github.com/google/wire"
)
type Foo struct {
X int
}
// ProvideFoo はFoo構造体を提供するNew関数
func ProvideFoo() Foo {
return Foo{X: 42}
}
type Bar struct {
X int
}
// ProvideBar はFooを用いてBarを初期化するNew関数
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
type Baz struct {
X int
}
// ProvideBaz はBarを利用してBazを初期化するNew関数
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}
// wireを使ってFoo | Bar | Bazを提供するDIセットを定義する。
// Bazを取得するときは別にcontext.Contextを用意する必要がある。
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
上記のサンプルコードの最後で定義しているのがWireの仕組み内でProviderと呼ばれるもので、このProviderを複数組み合わせることでDIの初期化をしていく。
Wireを使ったDIの宣言
wire.Build
を使って依存性を収入するオブジェクトの構成(Injector)を定義する。
// +build wireinject
// このコードはgo buildには含まない
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// SuperSetを使ってDI済みのBazを生成する。
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.SuperSet)
return foobarbaz.Baz{}, nil
}
この例ではBaz
オブジェクトを生成する関数を定義している。
前述の中で定義したSuperSet
に含まれるBaz
オブジェクトを生成する関数(ProvideBar
)に必要なcontext.Context
がSuperSet
内の関数から用意できないため、initializeBaz
の引数からcontext.Context
を与えている。
なお、「あるSetからXXXオブジェクトを生成するためにはどんなオブジェクトを用意すればいいのか?」はwire show
コマンドを使えば簡単に調べることができる。
$ wire show
"github.com/budougumi0617/foobarbaz".SuperSet
Outputs given no inputs: # 特に用意せずにSuperSetから生成できるオブジェクト
github.com/budougumi0617/foobarbaz.Bar
at /Users/budougumi0617/go/src/github.com/budougumi0617/foobarbaz/foobarbaz.go:24:6
github.com/budougumi0617/foobarbaz.Foo
at /Users/budougumi0617/go/src/github.com/budougumi0617/foobarbaz/foobarbaz.go:15:6
Outputs given context.Context: # context.Contextを与えれば生成できるオブジェクト
github.com/budougumi0617/foobarbaz.Baz
at /Users/budougumi0617/go/src/github.com/budougumi0617/foobarbaz/foobarbaz.go:33:6
Wireを使ってDIの自動生成
先程のinitializeBaz
から実際にgo build
時に利用するDIコードを自動生成する。
wire gen
コマンドを使ってinitializeBaz
から自動生成されたコードが以下。
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"example.com/foobarbaz"
)
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
foo := foobarbaz.ProvideFoo()
bar := foobarbaz.ProvideBar(foo)
baz, err := foobarbaz.ProvideBaz(ctx, bar)
if err != nil {
return 0, err
}
return baz, nil
}
こうしてwire gen
コマンドを使うと、DI済みのオブジェクトを作成するコードを自動生成してくれる。
このwire gen
を使ったDIのよいところは以下だと感じている。
- 必要な依存関係を含めておけばwireが適切な順序でオブジェクト生成、DIを繰り返すコードを自動生成してくれる
- 不足している場合は
wire gen
時に何が足りないかエラー出力してくれる
- 不足している場合は
- コード上の仕組みはシンプルなので後からでも導入できる。また逆に脱却も用意
- 「ただオブジェクト間の依存性を解決するだけ」という人間が行なう必要が無いコードをWireに一存できる
最後の利点については、例えばgo-cloud
のサンプルコードを見るとその効果がわかりやすい。
go-cloud
のサンプルコードではGCPを利用したWebサービスのDIをWireのInjectorの仕組みを使って以下のように設定している。
詳細は説明しないが、Bucket
やCloud SQL
、Stackdriver
の依存性を注入したWebサーバーオブジェクト(*application
)を生成するDIの定義だ。
/github.com/google/go-cloud/samples/guestbook/inject_gcp.go
// setupGCP is a Wire injector function that sets up the application using GCP.
func setupGCP(ctx context.Context, flags *cliFlags) (*application, func(), error) {
// This will be filled in by Wire with providers from the provider sets in
// wire.Build.
wire.Build(
gcpcloud.GCP,
cloudmysql.Open,
applicationSet,
gcpBucket,
gcpMOTDVar,
gcpSQLParams,
)
return nil, nil, nil
}
このsetupGCP
からwire gen
コマンドを使って実際にビルドで利用するDIコードを自動生成すると、70行近いDIコードが自動生成される(コードの掲載は省略する)。
https://github.com/google/go-cloud/samples/guestbook/wire_gen.go#L105-L171
依存関係が変更あるいは追加されるたびにこのような依存関係を自動で再生成してくれるのは非常にありがたい。
(再生成前にwire diff
コマンドを使って自動生成コードの差分を確認することも可能だ。)
wireコマンドで利用できるDI関数の戻り値シグネチャ
上記のようにDIを定義するWireだが、Provider(wire.NewSet
)で利用できるDI定義の関数には条件がある。
(依存するオブジェクトは生成する各々のオブジェクトで異なるので当然だが、)関数の引数には制限はないが、戻り値のシグネチャは4パターンしか認められていない。
func() Object
1つ目は単純にDI済みのオブジェクトのみを返すパターンだ。
例: github.com/google/go-cloud/gcp/gcp.go#L79
func CredentialsTokenSource(creds *google.Credentials) TokenSource
func() (Object, error)
2つ目はDIに失敗したときにerror
を返す戻り値パターンだ。error
を返すDI関数がwire.Build
に含まれている場合は、自動生成後のコード内で適切にエラーハンドリングするコードを生成してくれる。
例: github.com/google/go-cloud/gcp/gcp.go#L70
func DefaultCredentials(ctx context.Context) (*google.Credentials, error)
func() (Object, func())
3つ目は廃棄前に何らかのクリーンアップ処理が必要なDI済みのオブジェクトを生成する関数に対応するパターン。
例えば*sql.DB
オブジェクトを生成する関数を用意するとして、db.close
をクリーンアップ関数として返しておく。
そうすると、自動生成されたDIコードの中でerr != nil
だったとき適切にクリーンアップが行われる。
例: https://github.com/google/go-cloud/samples/guestbook/main.go#L313
func appHealthChecks(db *sql.DB) ([]health.Checker, func())
func() (Object, func(), error)
4つ目は異常がなかったときはDIオブジェクトとクリーンアップ関数を返し、異常時はerror
を返すパターン。
実際に定義する際は3つめのパターンよりこの宣言になることのほうが多いだろう。
例: https://github.com/google/go-cloud/mysql/rdsmysql/rdsmysql.go#L60
func appHealthChecks(db *sql.DB) ([]health.Checker, func())
どれもXXX
構造体に対してNewXXX
関数を定義するときに利用されるパターンだ。なので、既存コードのNew
関数を使ってwire.NewSet
を定義していけば既存のプロジェクトでもすぐに利用を開始できる。
終わりに
今回はWire
を使ったDIの利点とWire
で利用可能なDI関数の戻り値パターンを紹介した。
プリミティブな値を引数・戻り値に含む関数を使ってDIをするときは型エイリアスを適切に切っておく、などいくつかのプラクティスがあるのだが、それらはWireのリポジトリのdocsを確認すればよい。
実際にWireを使ったサンプルコードについては以下を参照するといいと思う。
- https://github.com/google/go-cloud/tree/master/samples/guestbook
- https://github.com/terashi58/wire-example
なお、プロダクトで利用している事例は私の同僚のterashiさんが書いた以下のブログ記事が詳しい。
- freeeのマイクロサービス基盤とWire導入
参考
- google/wire
- google/go-cloud
- Compile-time Dependency Injection With Go Cloud’s Wire
- freeeのマイクロサービス基盤とWire導入