Skip to content

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
go install github.com/google/wire/cmd/wire@latest

Installa le dipendenze del codice sorgente

go get github.com/google/wire

Introduzione

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.

go
package foobarbaz

type Foo struct {
    X int
}

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

Con parametri

go
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

go
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

go
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 essere error

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.

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +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

wire

verrà generato wire_gen.go, il contenuto è il seguente

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

go
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

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
}

Dopo aver definito i provider, è necessario definire gli injector. È meglio creare un nuovo file wire.go per definire

go
//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.

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

Il codice dopo la generazione è il seguente

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:

// 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.

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

L'output finale è il seguente

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

Questo è 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.

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

go
func Bind(iface, to interface{}) Binding
go
var 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.

go
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

go
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.

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

Esempio:

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 generato potrebbe essere simile al seguente

go
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

go
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

go
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.

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
}

Il codice effettivamente generato potrebbe essere simile al seguente

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
}

Duplicazione di Tipo

È meglio non avere parametri di tipo duplicato nei provider, specialmente per alcuni tipi di 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))
}

In questo caso la generazione del codice segnalerà un errore

provider has multiple parameters of type string

wire non sarà in grado di distinguere come iniettare questi parametri. Per evitare conflitti, si può usare un alias di tipo.

Golang by www.golangdev.cn edit