Skip to content

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
go install github.com/google/wire/cmd/wire@latest

ソースコード依存関係のインストール

go get github.com/google/wire

はじめに

wire における依存性注入は 2 つの要素に基づいています:providerinjector です。

provider は開発者によって提供されるコンストラクタで、以下のように示されます。Provider はエクスポートされている必要があります。

go
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo constructs a Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

パラメータ付き

go
package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

パラメータと戻り値を持つこともできます

go
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 を結合することもできます

go
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 を呼び出すことで宣言され、この宣言は呼び出されてはならず、コンパイルもされません。

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +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 が生成され、以下の内容になります:

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 を返します

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

func NewHttpserver(addr net.Addr) (*HttpServer, error) {
  return &HttpServer{addr}, nil
}

以下の MysqlClientSystem provider も同様です

go
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
//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 を無視するためのものです。その後、以下のコマンドを実行します。以下のように出力されれば生成成功です。

sh
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go

生成されたコードは以下の通りです

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 を呼び出してアプリを起動します。

go
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 は依存性注入の際、型に基づいて依存関係をマッチングします。

go
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 番目は具体的な実装ポインタ型です。

go
func Bind(iface, to interface{}) Binding
go
var Set = wire.NewSet(
    provideMyFooer,
    wire.Bind(new(Fooer), new(*MyFooer)),
    provideBar)

値バインディング

wire.Build を使用する際、値を提供するために provider を使用する必要はありません。wire.Value を使用して具体的な値を提供することもできます。wire.Value は値を構築するための式をサポートしており、この式はコード生成時に injector にコピーされます。以下のようになります。

go
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 を使用できます

go
func injectReader() io.Reader {
    wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
    return nil
}

構造体構築

providerset 内では、wire.Struct を使用して、他の provider からの戻り値を使用して指定された型の構造体を構築できます。

最初のパラメータは構造体ポインタ型で、その後ろにフィールド名が続きます。

go
func Struct(structType interface{}, fieldNames ...string) StructProvider

例:

go
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 は以下のようになります

go
func injectFooBar() FooBar {
    foo := ProvideFoo()
    bar := ProvideBar()
    fooBar := FooBar{
        MyFoo: foo,
        MyBar: bar,
    }
    return fooBar
}

すべてのフィールドを埋めたい場合は、* を使用できます。例えば

go
wire.Struct(new(FooBar), "*")

デフォルトでは構造体型を構築します。ポインタ型を構築したい場合は、injector シグネチャの戻り値を変更します

func injectFooBar() *FoodBar {
    wire.Build(Set)
}

フィールドを無視したい場合は、タグを追加できます。以下のようになります

go
type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}

クリーンアップ

provider によって構築された値が使用後にクリーンアップ作業(ファイルクローズなど)を必要とする場合、provider はクロージャを返してそのような操作を実行できます。injector はこのクリーンアップ関数を呼び出しません。いつ呼び出すかは injector の呼び出し元に委ねられます。以下のようになります。

go
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
}

実際に生成されたコードは以下のようになります

go
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 のパラメータに重複した型がないようにするのが最適です。特に基本型の場合

go
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 string

wire はこれらのパラメータがどのように注入されるべきかを区別できません。競合を避けるには、型エイリアスを使用できます。

Golang学习网由www.golangdev.cn整理维护