Wire
wire è uno strumento di iniezione delle dipendenze open source di Google. Il concetto di iniezione delle dipendenze è molto popolare nel framework Spring di Java. Anche in Go esistono alcune librerie di iniezione delle dipendenze, come dig open source di Uber. Tuttavia, il concetto di iniezione delle dipendenze di wire non si basa sul meccanismo di riflessione del linguaggio. Strettamente parlando, wire è in realtà un generatore di codice. Il concetto di iniezione delle dipendenze si riflette solo nell'uso. In caso di problemi, questi possono essere individuati durante la generazione del codice.
Indirizzo repository: google/wire: Compile-time Dependency Injection for Go (github.com)
Indirizzo documentazione: wire/docs/guide.md at main · google/wire (github.com)
Installazione
Installa lo strumento di generazione codice
go install github.com/google/wire/cmd/wire@latestInstalla le dipendenze del codice sorgente
go get github.com/google/wireIntroduzione
L'iniezione delle dipendenze in wire si basa su due elementi: provider e injector.
provider può essere un costruttore fornito dallo sviluppatore, come segue. Il Provider deve essere esposto esternamente.
package foobarbaz
type Foo struct {
X int
}
// Costruisce Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Con parametri
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar restituisce un Bar: un Foo negativo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}Può anche avere parametri e valori di ritorno
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
// ProvideBaz restituisce un valore se Bar non è 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
}È anche possibile combinare i provider
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire ha le seguenti regole per i valori di ritorno del provider
- Il primo valore di ritorno è il valore fornito dal provider
- Il secondo valore di ritorno deve essere
func() | error - Il terzo valore di ritorno, se il secondo valore di ritorno è
func, allora il terzo valore di ritorno deve essereerror
injector è una funzione generata da wire, responsabile di chiamare i provider nell'ordine specificato. La firma dell'injector è definita dallo sviluppatore, wire genera il corpo della funzione specifico, attraverso la chiamata a wire.Build per dichiarare. Questa dichiarazione non dovrebbe essere chiamata, né tantomeno compilata.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +build wireinject
// Il tag di build assicura che lo stub non sia compilato nel build finale.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// injector definito
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}Poi esegui
wireverrà generato wire_gen.go, il contenuto è il seguente
// 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 effettivamente generato
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
}Il codice generato non ha quasi dipendenze da wire, può funzionare normalmente anche senza wire, e può essere rigenerato successivamente eseguendo go generate. Dopo di ciò, lo sviluppatore chiama l'injector effettivamente generato passando i parametri corrispondenti per completare l'iniezione delle dipendenze. L'intero processo del codice è piuttosto semplice, sembra solo fornire alcuni costruttori, poi generare una funzione che chiama i costruttori, e infine chiamare questa funzione passando i parametri. Non sembra fare nulla di particolarmente complesso, si potrebbe scrivere a mano allo stesso modo. Esatto, wire fa proprio questa cosa semplice, solo che invece di scriverlo a mano viene generato automaticamente. Secondo la filosofia di wire, l'iniezione delle dipendenze dovrebbe essere una cosa così semplice, non dovrebbe essere complicata.
Esempio
Di seguito un caso per approfondire la comprensione, è un esempio di inizializzazione di un'app.
Il provider HttpServer riceve un parametro net.Addr e restituisce un puntatore e error
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}I provider MysqlClient e System seguenti sono simili
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
}Dopo aver definito i provider, è necessario definire gli injector. È meglio creare un nuovo file wire.go per definire
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// injector definito
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// chiama i provider nell'ordine
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject serve per ignorare questo injector durante la compilazione. Poi esegui il seguente comando, se l'output è il seguente significa che la generazione è riuscita.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goIl codice dopo la generazione è il seguente
// 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:
// injector definito
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
}Si può vedere che la logica è molto chiara, anche l'ordine di chiamata è corretto. Infine, avvia l'app tramite l'injector generato.
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()
}L'output finale è il seguente
2023/08/01 19:20:48 app run on 127.0.0.1:8080Questo è un caso d'uso molto semplice.
Utilizzo Avanzato
Binding di Interfacce
A volte, durante l'iniezione delle dipendenze, un'implementazione concreta viene iniettata in un'interfaccia. wire corrisponde in base al tipo durante l'iniezione delle dipendenze.
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 sarà un *MyFooer.
return f.Foo()
}Il parametro del provider provideBar è un tipo di interfaccia, ma in realtà è *MyFooer. Per far sì che il provider corrisponda correttamente durante la generazione del codice, possiamo associare i due tipi, come segue
Il primo parametro è il tipo di puntatore all'interfaccia specifica, il secondo è il tipo di puntatore all'implementazione concreta.
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)Binding di Valori
Quando si usa wire.Build, non è necessario fornire valori tramite provider, si può anche usare wire.Value per fornire un valore specifico. wire.Value supporta espressioni per costruire valori, questa espressione verrà copiata nell'injector durante la generazione del codice, come segue.
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}L'injector generato
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)Se si desidera associare un valore di tipo interfaccia, si può usare wire.InterfaceValue
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}Costruzione di Struct
Nei provider set, si può usare wire.Struct per costruire una struct di un tipo specifico utilizzando i valori di ritorno di altri provider.
Il primo parametro dovrebbe essere il tipo di puntatore alla struct, seguito dai nomi dei campi.
func Struct(structType interface{}, fieldNames ...string) StructProviderEsempio:
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 generato potrebbe essere simile al seguente
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}Se si desidera popolare tutti i campi, si può usare *, ad esempio
wire.Struct(new(FooBar), "*")Per impostazione predefinita si costruisce il tipo struct, se si desidera costruire un tipo puntatore, si può modificare il valore di ritorno della firma dell'injector
func injectFooBar() *FoodBar {
wire.Build(Set)
}Se si desidera ignorare un campo, si può aggiungere un tag, come mostrato di seguito
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Cleanup
Se un valore costruito da un provider necessita di operazioni di pulizia dopo l'uso (ad esempio chiudere un file), il provider può restituire una closure per eseguire tale operazione. L'injector non chiama questa funzione di cleanup, spetta al chiamante dell'injector decidere quando chiamarla, come segue.
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
}Il codice effettivamente generato potrebbe essere simile al seguente
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
}Duplicazione di Tipo
È meglio non avere parametri di tipo duplicato nei provider, specialmente per alcuni tipi di 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))
}In questo caso la generazione del codice segnalerà un errore
provider has multiple parameters of type stringwire non sarà in grado di distinguere come iniettare questi parametri. Per evitare conflitti, si può usare un alias di tipo.
