Go-Cloudで利用されているDIツールWireについて
概要
Go-Cloud Projectで利用されている、 Wire
と呼ばれる
Dependency Injection Tool について触ってみました。
(README.md
を試した形です)
Install
wire
は go get でインストールします。
go get github.com/google/go-cloud/wire/cmd/wire
実装
基本的な使い方
ProviderとInjecterという2つの概念を持ちます
Provider
Providerは、依存させたいstructの実体を返却します。
例)
依存させたいstructの実態を定義します。
// foo.go type Foo struct{ Name string }
Provider関数を定義します。
func ProviderFoo(name string) Foo { return Foo{ Name: name, } }
Providerの利用元も実装します。
(今回は1つですが) ProviderSetを定義することで、複数のProviderをまとめて処理します。
main.go
// ProviderSet var SuperSet = wire.NewSet(ProviderFoo) func main() { // flagで名前を切り替えられるようにしてみます n := flag.String("n", "foo", "foo name") flag.Parse() foo, err := setUp(context.Background(), *n) if err != nil { log.Fatalln(err) } // fooの名前を出力 fmt.Println(foo.Name) }
Injector
Injectorは、Provider関数を利用して実体を実装へinjectします。
injectorのコードは仮実装を元に、wire
が生成してくれます。
例)
Injectorの仮実装を定義します。
コード生成の元コードのため、buildタグを付与して
build時に利用されないようにします。
injector.go
// buildタグを付与 //+build wireinject // 実装 func setUp(ctx context.Context, name string) (Foo, error) { // ProviderをBuildする wire.Build(SuperSet) // 特に意味はない return Foo{}, nil }
ここで、injectorの実装を生成していきます。
下記コマンドを実行します。
(go getで入れているので、バイナリが$GOPATH/bin配下に配置されてます。)
% wire
wire
コマンドを実行すると、下記のような wire_gen.go
が生成されます。
参考) wire_gen.go
// Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject package main import ( context "context" ) // Injectors from inject.go: func setUp(ctx context.Context, n string) (Foo, error) { foo := ProviderFoo(n) return foo, nil }
実行
wire_gen.go
が生成された段階で、buildして実行してみます。
% go build -o wire-sample
(補足)
buildタグで、!wireinject
となっているため go build
時は、
injector.go
の代わりにwire_gen.go
が利用されます。
実行
% ./wire-sample foo # ちゃんとフラグで渡した値が利用されている % ./wire-sample -n bar bar
ちなみに、一度 wire_gen.go
を生成したあとで、修正したい場合は、 go generate
を利用します。
# 再ビルド % go generate && go build -o wire-sample
Tips
① 独自型を定義してProviderへ渡す
Providerに、一般的な型(string, int, etc..)を利用すると、コードを生成する際に一意に定まらず、期待しない動作になる可能性があります。
そのため、独自型で定義してsetUpの引数に渡してあげることで、確実に狙ったProviderの引数に渡すことができます。
例)
injectしたいstruct
type Foo struct { Name: string } type Hoge struct { Hoge: string } type FooHoge struct { Foo Foo Hoge Hoge }
providerの実装
// 型定義 type fooName string func ProviderFoo(name fooName) Foo { return Foo{ Name: string(name), } } func ProviderHoge(name string) Hoge { return Hoge{ Name: name, } } func ProviderFooHoge(foo Foo, hoge Hoge) FooHoge { return FooHoge{ Foo: foo, Hoge: hoge, } } var SuperSet = wire.NewSet(ProviderFoo, ProviderHoge, ProviderFooHoge) func main() { fh, _ := setUp(ctx, "foo", "hoge") fmt.Println(fh.Foo.Name) fmt.Println(fh.Hoge.Name) }
injector.go
func setUp(ctx context.Context, fn fooName , n string) (FooHoge, error) { wire.Build(SuperSet) return Foo{}, nil }
↓ go generate
// generateした際に、stringのままだとFoo/Hogeの // どちらの名前かわからなくなってしまう func setUp(ctx context.Context, fn fooName , n string) (FooHoge, error) { foo := ProviderFoo(fn) hoge := ProviderHoge(fn) fooHoge := ProviderFooHoge(foo, hoge) reutnr fooHoge, nil }
所感
GoでDIする際に、あまりツールを利用できていなかったので、 利用が増えてくれば、実コードに入れるのもありかと思いました。 DIに関しては、 Guice を一度触れていたので、比較的すんなり入ってきました。 テスタビリティがきちんとあがっているかは、テスト書いてみて確認したいです。