Skip to content

Wire

Wire ist ein von Google open-source gestelltes Dependency Injection Tool. Das Konzept der Dependency Injection ist im Java Spring Framework sehr beliebt. In Go gibt es ebenfalls einige Dependency Injection Bibliotheken, wie z.B. das von Uber open-source gestellte dig. Allerdings basiert die Dependency Injection Philosophie von Wire nicht auf dem Reflexionsmechanismus der Sprache. Streng genommen ist Wire ein Code-Generator. Die Dependency Injection Philosophie zeigt sich nur in der Verwendung. Bei Problemen können diese während der Code-Generierung gefunden werden.

Repository: google/wire: Compile-time Dependency Injection for Go (github.com)

Dokumentation: wire/docs/guide.md at main · google/wire (github.com)

Installation

Installation des Code-Generierungstools:

go
go install github.com/google/wire/cmd/wire@latest

Installation der Quellcode-Abhängigkeit:

go get github.com/google/wire

Einstieg

In Wire basiert die Dependency Injection auf zwei Elementen: Provider und Injector.

Provider können von Entwicklern als Konstruktoren bereitgestellt werden, wie unten gezeigt. Provider müssen öffentlich zugänglich sein.

go
package foobarbaz

type Foo struct {
    X int
}

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

Mit Parametern:

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

Kann auch Parameter und Rückgabewerte haben:

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 können auch kombiniert werden:

go
package foobarbaz

import (
    // ...
    "github.com/google/wire"
)

// ...

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

TIP

Wire hat folgende Regeln für die Rückgabewerte von Providern:

  • Der erste Rückgabewert ist der vom Provider bereitgestellte Wert
  • Der zweite Rückgabewert muss func() | error sein
  • Der dritte Rückgabewert, wenn der zweite func ist, muss error sein

Injector ist eine von Wire generierte Funktion, die für das Aufrufen der Provider in der angegebenen Reihenfolge verantwortlich ist. Die Signatur des Injectors wird vom Entwickler definiert, Wire generiert den Funktionskörper. Die Deklaration erfolgt durch den Aufruf von wire.Build. Diese Deklaration sollte nicht aufgerufen und noch weniger kompiliert werden.

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"
)

// Definierter injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

Dann ausführen:

wire

Dies generiert wire_gen.go mit folgendem Inhalt:

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"
)

// Tatsächlich generierter 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
}

Der generierte Code hat fast keine Abhängigkeiten zu Wire und funktioniert auch ohne Wire normal. Durch nachfolgendes Ausführen von go generate kann er erneut generiert werden. Danach ruft der Entwickler den tatsächlich generierten Injector auf und übergibt die entsprechenden Parameter, um die Dependency Injection abzuschließen. Der gesamte Prozess ist sehr einfach - es werden nur einige Konstruktoren bereitgestellt, dann eine Funktion generiert, die diese Konstruktoren aufruft, und schließlich diese Funktion mit Parametern aufgerufen. Es scheint nichts besonders Komplexes zu sein, und manuelle Implementierung wäre genauso möglich. Genau das ist es, was Wire tut - eine einfache Aufgabe, die von manueller Schreibweise zu automatischer Generierung wird. Nach der Philosophie von Wire sollte Dependency Injection so einfach sein und nicht kompliziert werden.

Beispiel

Im Folgenden wird ein Fallstudie verwendet, um das Verständnis zu vertiefen. Dies ist ein Beispiel für die Initialisierung einer App.

Der Provider von HttpServer empfängt einen net.Addr Parameter und gibt einen Zeiger und error zurück:

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

Die Provider von MysqlClient und System sind ähnlich:

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
}

Nachdem die Provider definiert sind, muss der Injector definiert werden. Am besten erstellen Sie eine neue wire.go Datei:

go
//go:build wireinject
// +build wireinject

package main

import (
  "github.com/google/wire"
  "net"
)

// Definiere injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // Rufe provider in der richtigen Reihenfolge auf
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject wird verwendet, um diesen Injector beim Kompilieren zu ignorieren. Führen Sie dann folgenden Befehl aus, und bei folgender Ausgabe war die Generierung erfolgreich:

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

Der generierte Code:

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:

// Definierter 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
}

Die Logik ist sehr klar und die Aufrufreihenfolge ist korrekt. Starten Sie schließlich die App über den generierten 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()
}

Ausgabe:

2023/08/01 19:20:48 app run on 127.0.0.1:8080

Dies ist ein sehr einfaches Anwendungsbeispiel.

Erweiterte Verwendung

Schnittstellenbindung

Manchmal wird bei der Dependency Injection eine konkrete Implementierung in eine Schnittstelle injiziert. Wire führt die Dependency Injection basierend auf Typabgleich durch.

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()
}

Der Parameter des Providers provideBar ist ein Schnittstellentyp, der tatsächlich *MyFooer ist. Damit der Provider während der Codegenerierung korrekt abgeglichen werden kann, können wir die beiden Typen binden:

Der erste Parameter ist der konkrete Schnittstellenzeigertyp, der zweite ist der konkrete Implementierungszeigertyp.

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

Wertbindung

Bei der Verwendung von wire.Build können Sie statt eines Providers auch wire.Value verwenden, um einen konkreten Wert bereitzustellen. wire.Value unterstützt Ausdrücke zur Konstruktion von Werten. Dieser Ausdruck wird während der Codegenerierung in den Injector kopiert:

go
type Foo struct {
    X int
}

func injectFoo() Foo {
    wire.Build(wire.Value(Foo{X: 42}))
    return Foo{}
}

Generierter Injector:

func injectFoo() Foo {
    foo := _wireFooValue
    return foo
}

var (
    _wireFooValue = Foo{X: 42}
)

Wenn Sie einen Wert eines Schnittstellentyps binden möchten, können Sie wire.InterfaceValue verwenden:

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

Strukturkonstruktion

Im ProviderSet können Sie wire.Struct verwenden, um eine Struktur eines angegebenen Typs aus den Rückgabewerten anderer Provider zu konstruieren.

Der erste Parameter sollte ein Strukturzeigertyp sein, gefolgt von Feldnamen:

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

Beispiel:

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

Der generierte Injector könnte wie folgt aussehen:

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

Wenn Sie alle Felder füllen möchten, können Sie * verwenden:

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

Standardmäßig wird der Strukturtyp konstruiert. Wenn Sie einen Zeigertyp konstruieren möchten, ändern Sie den Rückgabewert der Injector-Signatur:

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

Wenn Sie Felder ignorieren möchten, können Sie ein Tag hinzufügen:

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

Cleanup

Wenn ein von einem Provider konstruierter Wert nach der Verwendung aufgeräumt werden muss (z.B. Schließen einer Datei), kann der Provider eine Closure für solche Operationen zurückgeben. Der Injector ruft diese Cleanup-Funktion nicht auf. Wann sie aufgerufen wird, liegt beim Aufrufer des Injectors:

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
}

Der tatsächlich generierte Code könnte wie folgt aussehen:

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
}

Typ-Duplikate

Die Eingabeparameter eines Providers sollten keine Typ-Duplikate haben, besonders bei Basistypen:

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

In diesem Fall wird bei der Codegenerierung ein Fehler gemeldet:

provider has multiple parameters of type string

Wire kann nicht unterscheiden, wie diese Parameter injiziert werden sollen. Um Konflikte zu vermeiden, können Sie Typ-Aliase verwenden.

Golang by www.golangdev.cn edit