こんにちは。宿泊事業本部の宇都宮です。この記事では、GoのDIライブラリgoogle/wireの使い方を紹介します。
この記事は一休.com Advent Calendar 2019の9日目の記事です。
DIとは
DI(Dependency Injection, 依存性の注入)とは、あるオブジェクトが依存しているオブジェクトを自ら用意するのではなく、外部から渡してもらう(外部から注入する)というデザインパターンです。
例として、以下のように、監督の名前を渡すとその監督の映画を全てリストにして返すメソッドを持った構造体を考えます。
func (ml *MovieLister) MoviesDirectedBy(director string) []Movie {
allMovies := ml.finder.FindAll()
result := make([]Movie, 0, len(allMovies))
for _, m := range allMovies {
if director == m.Director {
result = append(result, m)
}
}
return result
}
この構造体は finder というフィールドに FindAll() メソッドを持つ構造体を持っています。
type MovieLister struct {
finder MoviesFinder
}
type MoviesFinder interface {
FindAll() []Movie
}
このfinderは、通常の制御の流れだと、MovieListerが自分で初期化してセットすることになります。
func NewMovieLister() *MovieLister {
return &MovieLister{
finder: NewColonDelimitedMovieFinder("movies.txt"),
}
}
しかし、これではMovieListerは特定のFinderと密結合してしまいます。データがRDBにあろうと外部のAPIにあろうと関係なく取得できるようにするためには、FinderをMovieListerの外で初期化して、MovieListerに渡す必要があります。
func NewMovieLister(finder MoviesFinder) *MovieLister {
return &MovieLister{
finder: finder,
}
}
func main() {
finder := NewColonDelimitedMovieFinder("movies.txt")
ml := NewMovieLister(finder)
fmt.Println(ml.MoviesDirectedBy("George Lucas"))
}
このように、DIパターンを用いると、コードの依存関係が明確になったり柔軟になったりといったメリットがあります。
また、Clean ArchitectureやOnion Architectureといったアーキテクチャパターンは依存性逆転の原則に基づいており、このようなアーキテクチャパターンを使う上でもDIは必須条件になります。
DIのデメリットは、初期化が煩雑になることです。よくあるWebアプリケーションで考えても、
- HTTPハンドラはDomain Serviceに依存している
- Domain ServiceはRepositoryに依存している
- RepositoryはDBコネクションマネージャに依存している
- DBコネクションマネージャはconfigに依存している
- configは環境変数に依存している
といった具合になります(※実際にDomain Serviceが依存しているのはinterfaceだったりしますが、その辺は省略)。
そこで、Java、C#、PHPなど様々な言語で「DIコンテナ」と呼ばれるライブラリが開発されています。DIコンテナは、オブジェクトの初期化、管理、注入といった仕事を引き受けるライブラリで、DIパターンをベースにしたWebアプリケーションフレームワークも少なくありません(一休でも一部で使用している ASP.NET CoreはDIコンテナを内蔵しており、DIパターンがベースになっています)。
GoのDIライブラリ
Go製のDIライブラリは多数ありますが、いわゆる「DIコンテナ」とは違った、Goの言語特性に沿ったライブラリに人気があります。google/wireは2018年12月に公開されたGoogle製のDIライブラリで、2019年12月現在、(GitHubのスター数ベースで)最も人気のあるDIライブラリと思われます。
google/wire(以下、wire)の特徴は、go generateによるコード生成を通したDIである、という点です。wireが必要になるのは開発者の手元だけで、プロダクションコードでwireをimportする必要はありません。
もう一つの特徴は、コンストラクタ(wireにおいては Provider と呼ばれる、値を生成する関数)のシグネチャに制限が加わることです。そのため、ライブラリというよりはフレームワークである、と考えた方がよいでしょう。一定の制約を受け入れる代わりに利便性を享受することができます。
wireの使い方
wireを使うには、まず手元にwireをインストールする必要があります。
go get github.com/google/wire/cmd/wire
次に、依存関係を定義するファイルを用意します。このように、依存関係を解決する関数をwireではInjectorと呼びます。
package main
import "github.com/google/wire"
func initMovieLister(fileName string) *MovieLister {
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
return nil
}
ここで重要なのは、1行目の //+build wireinject
というビルドタグです。これによって、通常のビルド時には wire.go はビルド対象から除外されます。
また、wireでは wire.Build
関数の引数にProvider(コンストラクタ)を列挙します。wireはこれらの関数のシグネチャを調べて、依存関係を解決します。
ここで使っているProviderのシグネチャは以下のようになっています。
func NewColonDelimitedMovieFinder(fileName string) MoviesFinder
func NewMovieLister(finder MoviesFinder) *MovieLister
wireはこれらの関数のシグネチャを調べて、必要な依存関係を解決するためのコードを生成します。生成には、go get
でインストールした wire
コマンドを使います。
package main
func initMovieLister(fileName string) *MovieLister {
moviesFinder := NewColonDelimitedMovieFinder(fileName)
movieLister := NewMovieLister(moviesFinder)
return movieLister
}
このようにして生成した initMovieLister
はmainなどで普通に呼び出せます。
func main() {
ml := initMovieLister("movies.txt")
fmt.Println(ml.MoviesDirectedBy("George Lucas"))
}
なお、 wire.Build()
の引数は 順不同 です。↓のように前後を入れ替えても、生成結果は変わりません。
wire.Build(
NewColonDelimitedMovieFinder,
NewMovieLister,
)
Providerのエラーハンドリング
Providerは、単に値を返すだけでなく、エラーやクリーンアップ用の関数を返すこともできます。たとえば、NewColonDelimitedMovieFinderがエラーを返すとすると、以下のようなシグネチャになります。
func NewColonDelimitedMovieFinder(fileName string) (MoviesFinder, error)
これに合わせて、 initMovieLister
関数もエラーを返すようにします。
func initMovieLister(fileName string) (*MovieLister, error) {
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
return nil, nil
}
生成後のコードでも、エラーハンドリングが行われるようになります。
func initMovieLister(fileName string) (*MovieLister, error) {
moviesFinder, err := NewColonDelimitedMovieFinder(fileName)
if err != nil {
return nil, err
}
movieLister := NewMovieLister(moviesFinder)
return movieLister, nil
}
Injectorのカスタマイズ
wire.goには好きなProvider関数を定義できます。ここで定義したProviderはwire_gen.goにコピーされます。これを利用して、シグネチャ的にwireでは扱えない関数(たとえば、引数が2つあっていずれもstringであるような関数)をProviderにできます。
たとえば、go標準の sql.Open()
関数ですね。
func Open(driverName, dataSourceName string) (*DB, error)
このままではwireで使えないので、sql.OpenのラッパーをInjectorに用意します。
type DriverName string
type DataSourceName string
func provideDBConn(driver DriverName, dsn DataSourceName) (*sql.DB, error) {
return sql.Open(string(driver), string(dsn))
}
func initDBConn(driver DriverName, dsn DataSourceName) (*sql.DB, error) {
wire.Build(
provideDBConn,
)
return nil, nil
}
ここでは、文字列を型で区別可能にするため、独自型を定義しています。DB設定がDBConfigのような構造体にまとまっているなら、provideDBConn関数の引数にDBConfigを取って、そのフィールドをsql.Openに渡してもよいでしょう。
Provider Set
ProviderはSetという形でグループ化できます。
var movieListerSet = wire.NewSet(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
wire.Build(
movieListerSet,
)
Setの使用はオプショナルで、Setを使わなくても依存関係は解決できます。パッケージ名が衝突してエイリアスが必要になるような場面などでは、衝突を避けるためにSetを使うと便利でしょう。
インタフェースのバインド
当初の実装では、NewColonDelimitedMovieFinderはMoviesFinderインタフェースの値を返していますが、具体的な型(*ColonDelimitedMovieFinder)を返しても問題ありません。ただし、この場合、 wire.Bind()
を使って *ColonDelimitedMovieFinder
を MoviesFinderインタフェースに紐付ける必要があります。
func NewColonDelimitedMovieFinder(fileName string)*ColonDelimitedMovieFinder {}
func NewMovieLister(finder MoviesFinder) *MovieLister {}
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
wire.Bind(new(MoviesFinder), new(*ColonDelimitedMovieFinder)),
)
これによって、MoviesFinderインタフェースを要求するProviderには、*ColonDelimitedMovieFinder
が渡されるようになります。
構造体のフィールドを参照する
↓のような構造体があるとき、NewMovieの引数にはDirector.Nameを渡したいと考えています。
type Movie struct {
Director string
}
func NewMovie(director string) *Movie {
return &Movie{Director: director}
}
type Director struct {
Name string
}
func NewDirector(name string) *Director {
return &Director{Name: name}
}
このようなときは wire.FieldsOf()
を使います。
func initMovie() *Movie {
wire.Build(
NewMovie,
NewDirector,
wire.FieldsOf(new(*Director), "Name"),
)
return nil
}
生成後のコードはこんな感じ(Director.Nameは常に空文字列なので、実用的な例ではないですね。。。)。
func initMovie() *Movie {
director := NewDirector()
string2 := director.Name
movie := NewMovie(string2)
return movie
}
細かな注意点
値とポインタの違いに注意
wireを使ってるとたまにあるのが、値とポインタのズレです。
たとえば、↓のように、あるProviderはポインタを返し、別のProviderは値を取る、という風になっていると、wireは「No provider found for ColonDelimitedMovieFinder」のようなエラーを吐きます。
func NewColonDelimitedMovieFinder(fileName string) *ColonDelimitedMovieFinder
func NewMovieLister(finder ColonDelimitedMovieFinder) *MovieLister
戻り値か引数、いずれかの型が間違っているので、修正しましょう。
go runするときはwire_gen.goも一緒に
通常、 go run
時にはエントリーポイントの main.go だけを渡せばOKですが、wireを使っている際は wire_gen.go も合わせて渡す必要があります。
go run main.go wire_gen.go
このようにしないと、wire_gen.goで定義しているInjector関数が未定義になり、エラーになります。
おわりに
google/wire の使い方を紹介しました。主なユースケースは本記事で紹介した範囲で網羅できていると思います。
本記事で触れていないテクニックはまだあるので、興味のある方は User Guide や Best Practices にも目を通してみてください。