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 install github.com/google/wire/cmd/wire@latestInstallation der Quellcode-Abhängigkeit:
go get github.com/google/wireEinstieg
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.
package foobarbaz
type Foo struct {
X int
}
// Konstruiert Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Mit Parametern:
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:
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:
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() | errorsein - Der dritte Rückgabewert, wenn der zweite
funcist, musserrorsein
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.
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"
)
// Definierter injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}Dann ausführen:
wireDies generiert wire_gen.go mit folgendem Inhalt:
// 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:
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:
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: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:
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goDer generierte Code:
// 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:
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:8080Dies 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.
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.
func Bind(iface, to interface{}) Bindingvar 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:
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:
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:
func Struct(structType interface{}, fieldNames ...string) StructProviderBeispiel:
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:
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:
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:
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:
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:
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:
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 stringWire kann nicht unterscheiden, wie diese Parameter injiziert werden sollen. Um Konflikte zu vermeiden, können Sie Typ-Aliase verwenden.
