Carpe Diem

備忘録

GoのDIツールwireで知っておくと良いこと

概要

christina04.hatenablog.com

こういった設計のようにレイヤ間の依存関係を抽象化していると、DIの初期化処理が段々冗長になっていきます。

google/wireはそういったDIの初期化を自動的にやってくれるコード生成ツールです。
今回はwireを使う上で知っていると便利なところをまとめます。

環境

wireの使い方知らない人

以下のチュートリアルが非常に分かりやすいので一度やってみてください。

github.com

実務で知っておくと良いポイント

Injectorの用意の手順

小さい依存関係であれば頭の中で把握できてサクッとかけますが、巨大なリポジトリになってくると何が何に依存しているか分からなくなってきます。

そういった場合まず最終的に用意するInjectorを生成するProviderだけwire.Buildするコードを用意し、エラーメッセージに書かれた通りにProviderを埋めていくとスムーズに進められます。

以下のようなモデルで、最終的にNewFooBarBaz()を実行したい場合

type Foo int
type Bar int
type Baz int

func ProvideFoo() Foo {
        return Foo(1)
}

func ProvideBar() Bar {
        return Bar(2)
}

func ProvideBaz() Baz {
        return Baz(3)
}

func NewFooBarBaz(foo Foo, bar Bar, baz Baz) int {
        return int(foo) + int(bar) + int(baz)
}

まずはNewFooBarBazだけ書きます。

func initializeFooBarBaz() int {
        wire.Build(
                NewFooBarBaz,
        )
        return 0
}

この状態でwireすると、どの依存オブジェクトが足りないかを1つ1つ教えてくれます。

wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Foo
        needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Bar
        needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Baz
        needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)

Fooが足りないとあるので追加。

wire.Build(
        NewFooBarBaz,
        ProvideFoo,
)

もう一度wire

wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Bar
        needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Baz
        needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)

エラーが減りました。同じように足りないBarBazも追加していきます。

この例ではNewFooBarBazの引数だけ見ればすぐ分かっていいのですが、

  • Barは別のオブジェクトに依存している
  • Barが依存しているオブジェクトはさらに他のオブジェクトに依存している

という状態になってくると全部把握するのはなかなかツライです。
なのでこの手順で上から1つ1つ依存オブジェクトを埋めていくのをオススメします。

パッケージ毎にSetでまとめておく

実際の実装では様々なpackageに分かれています。
その場合どのproviderがどこにあるかを把握するのは大変です。

そういった場合は各package毎にproviderのグループを用意すると扱いやすくなります。

先ほどのモデルがmodel packageにあるとして、そのproviderを扱いたい場合

.
├── main.go
├── model
│   └── model.go
└── wire.go

model package内で以下のようにproviderグループを用意し、

var Set = wire.NewSet(
        NewFooBarBaz,
        ProvideFoo,
        ProvideBar,
        ProvideBaz,
)

injector側でそのグループを以下のように呼び出します。

func initializeFooBarBaz() int {
        wire.Build(model.Set)
        return 0
}

interfaceに依存している場合

CleanArchitectureでよく見る形ですが

type UserRepository interface {
        Save() error
}

type User struct {
}

func (u *User) Save() error {
        return nil
}

func NewUserRepository() *User {
        return &User{}
}

type UserService struct {
        repo UserRepository
}

func NewUserService(u UserRepository) *UserService {
        return &UserService{repo: u}
}

上記コードのように、

  • 依存しているのはinterface
  • Provider(New関数)が生成するのは実装したstruct

である場合、その依存オブジェクトが実装されているstructを指定(bind)する必要があります。

func initializeUserService() *UserService {
        wire.Build(
                NewUserService,
                NewUserRepository,
                wire.Bind(new(UserRepository), new(*User)),  // ←
        )
        return &UserService{}
}

このようにwire.Bind()で実装したstructを指定します。

これはgoがduck typeを採用しており、実は同じメソッドを実装していたみたいなことが起きるため、間違ったオブジェクトを注入しないようにするためです。

structへの注入

先ほどはinterfaceのケースでしたが、structへ直接注入する方法もあります。

例えば以下のようなstructと各providerがあるとします。

type Foo int
type Bar int
type Baz int

func ProvideFoo() Foo {
        return Foo(1)
}

func ProvideBar() Bar {
        return Bar(2)
}

func ProvideBaz() Baz {
        return Baz(3)
}

type FooBarBaz struct {
        MyFoo Foo
        MyBar Bar
        MyBaz Baz
}

全フィールドに注入したい場合

wire.Struct()*を使って注入できます。

func initializeFooBarBaz() *FooBarBaz {
        wire.Build(
                ProvideFoo,
                ProvideBar,
                ProvideBaz,
                wire.Struct(new(FooBarBaz), "*"),
        )
        return &FooBarBaz{}
}

wireを実行すると以下のファイルが生成されます。

func initializeFooBarBaz() *FooBarBaz {
        foo := ProvideFoo()
        bar := ProvideBar()
        baz := ProvideBaz()
        fooBarBaz := &FooBarBaz{
                MyFoo: foo,
                MyBar: bar,
                MyBaz: baz,
        }
        return fooBarBaz
}

一部フィールドに注入したい場合

例えばMyFooMyBarフィールドだけ注入したい場合は

func initializeFooBarBaz() *FooBarBaz {
        wire.Build(
                ProvideFoo,
                ProvideBar,
                wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),
        )
        return &FooBarBaz{}
}

このようにFooとBar providerのみにして、wire.Structでフィールドを指定します。

wireを実行すると以下のファイルが生成されます。

func initializeFooBarBaz() *FooBarBaz {
        foo := ProvideFoo()
        bar := ProvideBar()
        fooBarBaz := &FooBarBaz{
                MyFoo: foo,
                MyBar: bar,
        }
        return fooBarBaz
}

各エラー

よく見るエラーをそれぞれケースに応じて説明します。

no provider found for xxx

以下のように必要なProviderが入っていないと

wire.Build(
        ProvideFoo,
        wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),  // ←MyBarのproviderがない
)

no provider foundエラーが発生します。

wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/struct/partial.Bar
        needed by *github.com/jun06t/go-sample/wire/struct/partial.FooBarBaz in struct provider "FooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:19:6)

multiple bindings for xxx

以下のように同じ依存オブジェクトを生成するProviderが含まれている

wire.Build(
        ProvideFoo,
        ProvideBar,
        ProvideBar,  // ←
        wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),
)

multiple bindingsエラーが出ます。

wire: /xxx/wire.go:6:2: multiple bindings for github.com/jun06t/go-sample/wire/struct/partial.Bar
        current:
        <- provider "ProvideBar" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:11:6)
        previous:
        <- provider "ProvideBar" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:11:6)

この例では同じ関数なのですぐに分かりますが、たくさんのパッケージでNewSet()で管理していたらいつの間にか重複するProviderが出てたりします。

またDBのmasterとslaveといった形で接続先を変えたい場合に

func NewDataRepository(master, slave *sql.DB) DataRepository {
    return &repoImpl{
        master: master,
        slave:  slave,
    }
}

func NewMaster() *sql.DB { /* .. */ }

func NewSlave() *sql.DB { /* .. */ }

として

wire.Build(
        NewMaster,
        NewSlave,
        NewDataRepository,
)

としてもmultiple bindingsエラーが出ます。wireは同じ型の依存オブジェクトを生成するProviderを許容しないためです。

What if my dependency graph has two dependencies of the same type?

公式では回避策として型を別途定義するよう薦めています。

struct has multiple fields of type

structに同じ型のフィールドを入れていると

type FooBarBaz struct {
        MyFoo  Foo
        MyFoo2 Foo
}

func initializeFooBarBaz() *FooBarBaz {
        wire.Build(
                ProvideFoo,
                wire.Struct(new(FooBarBaz), "*"),
        )
        return &FooBarBaz{}
}

struct has multiple fields of typeエラーが出ます。

wire: /xxx/model.go:20:2: provider struct has multiple fields of type github.com/jun06t/go-sample/wire/struct/partial.Foo

先ほどと同じように、wireがどのフィールドにどのオブジェクトを注入すればいいか分からなくなるためです。

複数のProviderで依存オブジェクトが重複するのは問題ない

注意なのが1つのProviderの中で重複する引数や重複するフィールドを持っているとNGなだけであって、複数のProviderで依存オブジェクトが重複するのは問題ないです。

type Foo int
type Bar int
type Baz int

func ProvideFoo() Foo {
        return Foo(1)
}

func ProvideBar(f Foo) Bar {
        return Bar(f)
}

func ProvideBaz(f Foo) Baz {
        return Baz(f)
}

func NewBarBaz(bar Bar, baz Baz) int {
        return int(bar) + int(baz)
}

このようにProvideBarProvideBazは同じ依存オブジェクトを持っていますが

func initializeBarBaz() int {
        wire.Build(
                ProvideFoo,
                ProvideBar,
                ProvideBaz,
                NewBarBaz,
        )
        return 0
}

これを実行しても問題なくビルドできます。

unused provider "xxx"

以下のように依存していない余分なproviderが入っていると

wire.Build(
        ProvideFoo,
        ProvideBar,
        ProvideBaz,  // ←
        wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),
)

unused providerエラーが発生します。

wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: unused provider "foo.ProvideBaz"
wire: github.com/jun06t/go-sample/wire/struct/partial: generate failed
wire: at least one generate failure

Bazは不要なのにProviderBazを含めてしまったせいですね。

NewSetでまとめる回避策

回避したい場合、不要なproviderを消すのは当然浮かびますが、NewSetを使って1つにまとめるという手法もあります。

Feature request: ignore unused provider set · Issue #227 · google/wire · GitHub

func initializeFooBarBaz() *FooBarBaz {
        wire.Build(
                Set,
        )
        return &FooBarBaz{}
}

var Set = wire.NewSet(
        ProvideFoo,
        ProvideBar,
        ProvideBaz,
        wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),
)

これで実行するとコケず、wire_gen.goは必要なものだけに絞られて生成されます。

func initializeFooBarBaz() *FooBarBaz {
        foo := ProvideFoo()
        bar := ProvideBar()
        fooBarBaz := &FooBarBaz{
                MyFoo: foo,
                MyBar: bar,
        }
        return fooBarBaz
}

// wire.go:

var Set = wire.NewSet(
        ProvideFoo,
        ProvideBar,
        ProvideBaz, wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"),
)

サンプルコード

今回のサンプルコードは以下にあります。

github.com