Skip to content

Wire

wire est un outil d'injection de dépendances open source de Google. Le concept d'injection de dépendances est très populaire dans le framework Spring de Java, et il existe également des bibliothèques d'injection de dépendances en Go, comme dig open source par Uber. Cependant, le concept d'injection de dépendances de wire n'est pas basé sur le mécanisme de réflexion du langage. Strictement parlant, wire est en fait un générateur de code. Le concept d'injection de dépendances ne se manifeste que dans l'utilisation. S'il y a des problèmes, ils peuvent être détectés pendant la génération du code.

Adresse du dépôt : google/wire: Compile-time Dependency Injection for Go (github.com)

Adresse de la documentation : wire/docs/guide.md at main · google/wire (github.com)

Installation

Installer l'outil de génération de code

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

Installer les dépendances du code source

go get github.com/google/wire

Démarrage

L'injection de dépendances dans wire est basée sur deux éléments : provider et injector.

Le provider peut être un constructeur fourni par le développeur, comme ci-dessous. Le Provider doit être exposé à l'extérieur.

go
package foobarbaz

type Foo struct {
    X int
}

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

Avec des paramètres

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

Peut également avoir des paramètres et des valeurs de retour

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
}

Il est également possible de combiner des providers

go
package foobarbaz

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

// ...

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

TIP

wire a les règles suivantes pour les valeurs de retour du provider

  • La première valeur de retour est la valeur fournie par le provider
  • La deuxième valeur de retour doit être func() | error
  • La troisième valeur de retour, si la deuxième est func, doit être error

L'injector est une fonction générée par wire, responsable d'appeler les providers dans l'ordre spécifié. La signature de l'injecteur est définie par le développeur, wire génère le corps de la fonction. On le déclare en appelant wire.Build, cette déclaration ne doit pas être appelée, et encore moins compilée.

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

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

Puis exécutez

wire

Cela générera wire_gen.go, avec le contenu suivant

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

// injector réellement généré
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
}

Le code généré n'a presque aucune dépendance envers wire, il peut fonctionner normalement sans wire. Ensuite, en exécutant go generate, on peut le régénérer. Après cela, le développeur appelle l'injecteur réellement généré en passant les paramètres correspondants pour compléter l'injection de dépendances. Tout le processus semble assez simple, il suffit de fournir quelques constructeurs, puis générer une fonction qui appelle ces constructeurs, et enfin appeler cette fonction en passant les paramètres. On n'a pas l'impression d'avoir fait quelque chose de particulièrement complexe, on pourrait l'écrire à la main. C'est exactement ça, wire fait simplement cette chose simple, mais au lieu d'écrire à la main, c'est généré automatiquement. Selon la philosophie de wire, l'injection de dépendances devrait être simple, sans être complexifiée.

Exemple

Voici un exemple pour approfondir la compréhension, c'est un exemple d'initialisation d'une application.

Le provider de HttpServer prend un paramètre net.Addr et retourne un pointeur et une error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

De même pour les providers de MysqlClient et System ci-dessous

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
}

Une fois les providers définis, il faut définir l'injector. Il est préférable de créer un fichier wire.go pour le définir

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

package main

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

// Définition de l'injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // Appel des providers dans l'ordre
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject est pour ignorer cet injector lors de la compilation. Puis exécutez la commande suivante, si vous voyez cette sortie, la génération a réussi.

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

Le code généré est le suivant

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:

// Définition de l'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
}

On peut voir que la logique est très claire, l'ordre d'appel est également correct. Enfin, on utilise l'injector généré pour démarrer l'application.

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

Sortie finale

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

C'est un exemple d'utilisation très simple.

Utilisation avancée

Liaison d'interface

Parfois, lors de l'injection de dépendances, on injecte une implémentation concrète dans une interface. Wire effectue l'injection de dépendances en se basant sur la correspondance des types.

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

Le paramètre du provider provideBar est un type interface, qui est en fait *MyFooer. Pour que le provider puisse correctement correspondre lors de la génération du code, nous pouvons lier les deux types, comme suit

Le premier paramètre est le type pointeur d'interface spécifique, le second est le type pointeur de l'implémentation concrète.

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

Liaison de valeur

Lors de l'utilisation de wire.Build, on peut utiliser wire.Value pour fournir une valeur concrète au lieu d'un provider. wire.Value supporte les expressions pour construire des valeurs, cette expression sera copiée dans l'injector lors de la génération du code, comme suit.

go
type Foo struct {
    X int
}

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

Injector généré

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

var (
    _wireFooValue = Foo{X: 42}
)

Si vous voulez lier une valeur de type interface, vous pouvez utiliser wire.InterfaceValue

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

Construction de structure

Dans un providerset, vous pouvez utiliser wire.Struct pour construire une structure d'un type spécifié en utilisant les valeurs de retour d'autres providers.

Le premier paramètre doit être un type pointeur de structure, suivi des noms de champs.

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

Exemple ci-dessous

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

L'injector généré pourrait ressembler à

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

Si vous voulez remplir tous les champs, vous pouvez utiliser *, par exemple

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

Par défaut, cela construit un type structure. Si vous voulez construire un type pointeur, vous pouvez modifier la valeur de retour de la signature de l'injector

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

Si vous voulez ignorer un champ, vous pouvez ajouter un tag, comme montré ci-dessous

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

Cleanup

Si une valeur construite par un provider nécessite un travail de nettoyage après utilisation (comme fermer un fichier), le provider peut retourner une closure pour effectuer cette opération. L'injector n'appelle pas cette fonction de cleanup, c'est à l'appelant de l'injector de décider quand l'appeler, comme suit.

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
}

Le code réellement généré pourrait ressembler à

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
}

Types en double

Les paramètres d'entrée du provider ne devraient pas avoir de types en double, surtout pour les types de base

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

Dans ce cas, la génération du code provoquera une erreur

provider has multiple parameters of type string

Wire ne pourra pas distinguer comment ces paramètres doivent être injectés. Pour éviter les conflits, vous pouvez utiliser des alias de type.

Golang by www.golangdev.cn edit