My External Storage

Dec 14, 2018 - 7 minute read - Comments - go

google/wireを使ったDIとDI関数のシグネチャについて #go

これはGo Advent Calendar 2018の14日目の記事となる。
この記事ではGoogleが提供するGoのDependency Injection(DI、依存性の注入)ツールであるWireを使ったDIの概要と、Wireで利用可能なDI関数の戻り値シグネチャのパターンを紹介する。

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だけ別のリポジトリに分離され、独立して管理されるようになった。 次節から説明するwirepkgを使った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)を定義する。

Injectors | Wire User Guideより

// +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.ContextSuperSet内の関数から用意できないため、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から自動生成されたコードが以下。

Injectors | Wire User Guideより

// 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の仕組みを使って以下のように設定している。 詳細は説明しないが、BucketCloud SQLStackdriverの依存性を注入した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を使ったサンプルコードについては以下を参照するといいと思う。

なお、プロダクトで利用している事例は私の同僚のterashiさんが書いた以下のブログ記事が詳しい。

参考

関連

関連記事