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 install github.com/google/wire/cmd/wire@latestInstaller les dépendances du code source
go get github.com/google/wireDé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.
package foobarbaz
type Foo struct {
X int
}
// Construit Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Avec des paramètres
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
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
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 êtreerror
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.
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"
)
// injector défini
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}Puis exécutez
wireCela générera wire_gen.go, avec le contenu suivant
// 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
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
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: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.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goLe code généré est le suivant
// 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.
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:8080C'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.
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.
func Bind(iface, to interface{}) Bindingvar 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.
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
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.
func Struct(structType interface{}, fieldNames ...string) StructProviderExemple ci-dessous
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 à
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
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
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.
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 à
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
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 stringWire ne pourra pas distinguer comment ces paramètres doivent être injectés. Pour éviter les conflits, vous pouvez utiliser des alias de type.
