Wire
wire は Google がオープンソース化した依存性注入ツールです。依存性注入は Java の Spring フレームワークで普及した概念です。Go にもいくつかの依存性注入ライブラリがあります。例えば Uber がオープンソース化した dig などです。しかし、wire の依存性注入アプローチは言語のリフレクションメカニズムに基づいていません。厳密に言えば、wire は実際にはコードジェネレーターで、依存性注入の概念はその使い方にのみ反映されています。問題があれば、コード生成時に見つけることができます。
リポジトリ:google/wire: Compile-time Dependency Injection for Go (github.com)
ドキュメント:wire/docs/guide.md at main · google/wire (github.com)
インストール
コード生成ツールのインストール
go install github.com/google/wire/cmd/wire@latestソースコード依存関係のインストール
go get github.com/google/wireはじめに
wire における依存性注入は 2 つの要素に基づいています:provider と injector です。
provider は開発者によって提供されるコンストラクタで、以下のように示されます。Provider はエクスポートされている必要があります。
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo constructs a Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}パラメータ付き
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}パラメータと戻り値を持つこともできます
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
// ProvideBaz returns a value if Bar is not zero.
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
}Provider を結合することもできます
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire には provider の戻り値に関する以下のルールがあります:
- 最初の戻り値は provider によって提供される値です
- 2 番目の戻り値は
func() | errorでなければなりません - 3 番目の戻り値は、2 番目の戻り値が
funcの場合、errorでなければなりません
injector は wire によって生成される関数で、指定された順序で provider を呼び出す責任があります。injector のシグネチャは開発者によって定義され、wire が具体的な関数本体を生成します。これは wire.Build を呼び出すことで宣言され、この宣言は呼び出されてはならず、コンパイルもされません。
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// Define injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}その後実行します
wireこれにより wire_gen.go が生成され、以下の内容になります:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//+build !wireinject
package main
import (
"example.com/foobarbaz"
)
// Actual generated injector
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 foobarbaz.Baz{}, err
}
return baz, nil
}生成されたコードは wire への依存がほとんどなく、wire がなくても正常に動作します。その後 go generate を実行して再生成できます。その後、開発者は対応するパラメータで実際に生成された injector を呼び出すことで依存性注入を完了します。全体のプロセスは非常にシンプルではありませんか?いくつかのコンストラクタを提供し、コンストラクタを呼び出す関数を生成し、最後にこの関数をパラメータで呼び出すだけです。特に複雑なことは何もしていないように見え、手書きすることもできます。はい、まさにそれが wire の行うことです - このようなシンプルなことを自動化しているだけで、手書きする代わりに。wire の哲学によれば、依存性注入はこのようなシンプルなことであり、複雑であるべきではありません。
例
理解を深めるための例を示します。これはアプリを初期化する例です。
HttpServer の provider は net.Addr パラメータを受け取り、ポインタと error を返します
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}以下の MysqlClient と System provider も同様です
var DataBaseProviderSet = wire.NewSet(NewMysqlClient)
type MysqlClient struct {
}
var SystemSet = wire.NewSet(NewApp)
type System struct {
server *HttpServer
data *MysqlClient
}
func (s *System) Run() {
log.Printf("app run on %s", s.server.String())
}
func NewApp(server *HttpServer, data *MysqlClient) (System, error) {
return System{server: server, data: data}, nil
}provider を定義した後、injector を定義する必要があります。新しい wire.go ファイルを作成して定義するのが最適です
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// Define injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// Call providers in order
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject は、コンパイル時にこの injector を無視するためのものです。その後、以下のコマンドを実行します。以下のように出力されれば生成成功です。
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go生成されたコードは以下の通りです
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"net"
)
// Injectors from wire.go:
// Define injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
httpServer, err := NewHttpserver(serverAddr)
if err != nil {
return System{}, err
}
mysqlClient, err := NewMysqlClient(dataAddr)
if err != nil {
return System{}, err
}
system, err := NewApp(httpServer, mysqlClient)
if err != nil {
return System{}, err
}
return system, nil
}ロジックが非常に明確で、呼び出し順序も正しいことがわかります。最後に、生成された injector を呼び出してアプリを起動します。
package main
import (
"github.com/google/wire"
"log"
"net"
"net/netip"
)
func main() {
server, err := initSystemServer(
net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8080")),
"mysql:localhost:3306/test")
if err != nil {
panic(err)
}
server.Run()
}最終出力は以下の通りです
2023/08/01 19:20:48 app run on 127.0.0.1:8080これは非常にシンプルな使用例です。
高度な使い方
インターフェースバインディング
依存性注入の過程で、具体的な実装がインターフェースに注入されることがあります。wire は依存性注入の際、型に基づいて依存関係をマッチングします。
type Fooer interface {
Foo() string
}
type MyFooer string
func (b *MyFooer) Foo() string {
return string(*b)
}
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}
type Bar string
func provideBar(f Fooer) string {
// f will be a *MyFooer.
return f.Foo()
}provider provideBar のパラメータはインターフェース型ですが、実際には *MyFooer です。コード生成時に provider が正しくマッチングするようにするには、2 つの型を一緒にバインドできます。以下のようになります:
最初のパラメータは具体的なインターフェースポインタ型で、2 番目は具体的な実装ポインタ型です。
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)値バインディング
wire.Build を使用する際、値を提供するために provider を使用する必要はありません。wire.Value を使用して具体的な値を提供することもできます。wire.Value は値を構築するための式をサポートしており、この式はコード生成時に injector にコピーされます。以下のようになります。
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}生成された injector
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)インターフェース型の値をバインドしたい場合は、wire.InterfaceValue を使用できます
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}構造体構築
providerset 内では、wire.Struct を使用して、他の provider からの戻り値を使用して指定された型の構造体を構築できます。
最初のパラメータは構造体ポインタ型で、その後ろにフィールド名が続きます。
func Struct(structType interface{}, fieldNames ...string) StructProvider例:
type Foo int
type Bar int
func ProvideFoo() Foo {/* ... */}
func ProvideBar() Bar {/* ... */}
type FooBar struct {
MyFoo Foo
MyBar Bar
}
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "MyFoo", "MyBar"))
func injectFooBar() FoodBar {
wire.Build(Set)
}生成された injector は以下のようになります
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}すべてのフィールドを埋めたい場合は、* を使用できます。例えば
wire.Struct(new(FooBar), "*")デフォルトでは構造体型を構築します。ポインタ型を構築したい場合は、injector シグネチャの戻り値を変更します
func injectFooBar() *FoodBar {
wire.Build(Set)
}フィールドを無視したい場合は、タグを追加できます。以下のようになります
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}クリーンアップ
provider によって構築された値が使用後にクリーンアップ作業(ファイルクローズなど)を必要とする場合、provider はクロージャを返してそのような操作を実行できます。injector はこのクリーンアップ関数を呼び出しません。いつ呼び出すかは injector の呼び出し元に委ねられます。以下のようになります。
type Data struct {
// TODO wrapped database client
}
// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{}, cleanup, nil
}実際に生成されたコードは以下のようになります
func wireApp(confData *conf.Data, logger log.Logger) (func(), error) {
dataData, cleanup, err := data.NewData(confData, logger)
if err != nil {
return nil, nil, err
}
// inject data
// ...
return app, func() {
cleanup()
}, nil
}型の重複
provider のパラメータに重複した型がないようにするのが最適です。特に基本型の場合
type FooBar struct {
foo string
bar string
}
func NewFooBar(foo string, bar string) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
func InitializeFooBar(a string, b string) FooBar {
panic(wire.Build(NewFooBar))
}この場合、コード生成はエラーを報告します
provider has multiple parameters of type stringwire はこれらのパラメータがどのように注入されるべきかを区別できません。競合を避けるには、型エイリアスを使用できます。
