Skip to content

Wire

wire es una herramienta de inyección de dependencias de código abierto de Google. El concepto de inyección de dependencias es muy popular en el marco Spring de Java, y Go también tiene algunas bibliotecas de inyección de dependencias, como dig de código abierto de Uber. Sin embargo, la filosofía de inyección de dependencias de wire no se basa en el mecanismo de reflexión del lenguaje. Estrictamente hablando, wire es en realidad un generador de código, la filosofía de inyección de dependencias solo se refleja en el uso. Si hay problemas, se pueden encontrar durante la generación de código.

Dirección del repositorio: google/wire: Compile-time Dependency Injection for Go (github.com)

Dirección de documentación: wire/docs/guide.md at main · google/wire (github.com)

Instalación

Instalar herramienta de generación de código

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

Instalar dependencias de código fuente

go get github.com/google/wire

Introducción

La inyección de dependencias en wire se basa en dos elementos: provider e injector.

provider puede ser un constructor proporcionado por el desarrollador, como sigue, Provider debe estar expuesto externamente.

go
package foobarbaz

type Foo struct {
    X int
}

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

Con parámetros

go
package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar devuelve un Bar: un Foo negativo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

También puede tener parámetros y valores de retorno

go
package foobarbaz

import (
    "context"
    "errors"
)

type Baz struct {
    X int
}

// ProvideBaz devuelve un valor si Bar no es cero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("no se puede proporcionar baz cuando bar es cero")
    }
    return Baz{X: bar.X}, nil
}

También se pueden combinar providers

go
package foobarbaz

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

// ...

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

TIP

wire tiene las siguientes reglas para los valores de retorno de provider

  • El primer valor de retorno es el valor proporcionado por provider
  • El segundo valor de retorno debe ser func() | error
  • El tercer valor de retorno, si el segundo valor de retorno es func, entonces el tercer valor de retorno debe ser error

injector es una función generada por wire, responsable de llamar a providers en el orden especificado. La firma del injector es definida por el desarrollador, wire genera el cuerpo de la función específico, se declara llamando a wire.Build, esta declaración no debe ser llamada, y no debe ser compilada.

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +build wireinject
// La etiqueta de construcción asegura que el stub no se compile en la compilación final.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

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

Luego ejecutar

wire

generará wire_gen.go, el contenido es el siguiente

go
// Código generado por Wire. NO EDITAR.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//+build !wireinject

package main

import (
    "example.com/foobarbaz"
)

// injector generado realmente
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
}

El código generado casi no tiene dependencias de wire, puede funcionar normalmente sin wire, y se puede generar nuevamente ejecutando go generate posteriormente. Después, el desarrollador completa la inyección de dependencias llamando al injector generado realmente y pasando los parámetros correspondientes. ¿No es todo el proceso de código bastante simple? Parece que solo es proporcionar algunos constructores, luego generar una función que llama a los constructores, y finalmente llamar a esta función pasando parámetros, parece que no se hizo nada particularmente complejo, se puede escribir a mano de la misma manera, sí, eso es exactamente lo que hace wire, solo que se convierte de escritura manual a generación automática. Según la filosofía de wire, la inyección de dependencias debería ser algo tan simple, no debería complicarse.

Ejemplo

A continuación hay un caso para profundizar la comprensión, es un ejemplo de inicialización de app.

El provider de HttpServer recibe un parámetro net.Addr, devuelve puntero y error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

Los providers de MysqlClient y System a continuación son similares

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
}

Después de definir los providers, es necesario definir el injector, es mejor crear un archivo wire.go para definir

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

package main

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

// definir injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // llamar providers en orden
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject es para ignorar este injector en tiempo de compilación. Luego ejecutar el siguiente comando, si la salida es la siguiente, significa que la generación fue exitosa.

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

El código después de la generación es el siguiente

go
// Código generado por Wire. NO EDITAR.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
  "net"
)

// Injectors from wire.go:

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

Se puede ver que la lógica es muy clara, el orden de llamada también es correcto. Finalmente, se usa el injector generado para iniciar la app.

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

La salida final es la siguiente

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

Este es un caso de uso muy simple.

Uso avanzado

Enlace de interfaz

A veces, durante la inyección de dependencias, se inyectará una implementación concreta en una interfaz. wire coincide por tipo durante la inyección de dependencias.

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 será un *MyFooer.
    return f.Foo()
}

El parámetro del provider provideBar es un tipo de interfaz, en realidad es *MyFooer. Para que el provider pueda coincidir correctamente durante la generación de código, podemos vincular los dos tipos, como sigue

El primer parámetro es el tipo de puntero de interfaz concreto, el segundo es el tipo de puntero de implementación concreta.

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

Enlace de valores

Al usar wire.Build, no es necesario que un provider proporcione valores, también se puede usar wire.Value para proporcionar un valor concreto. wire.Value soporta expresiones para construir valores, esta expresión se copiará en el injector durante la generación de código, como sigue.

go
type Foo struct {
    X int
}

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

El injector generado

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

var (
    _wireFooValue = Foo{X: 42}
)

Si se desea vincular un valor de tipo de interfaz, se puede usar wire.InterfaceValue

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

Construcción de estructuras

En providerset, se puede usar wire.Struct para construir una estructura de tipo especificado usando los valores de retorno de otros providers.

El primer parámetro debe ser el tipo de puntero de estructura, seguido de los nombres de campos.

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

Ejemplo como sigue

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

El injector generado puede ser como el siguiente

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

Si se desean completar todos los campos, se puede usar *, por ejemplo

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

Por defecto se construye el tipo de estructura, si se desea construir un tipo de puntero, se puede modificar el valor de retorno de la firma del injector

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

Si se desea ignorar un campo, se puede agregar una etiqueta, como se muestra a continuación

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

Cleanup

Si un valor construido por un provider necesita trabajo de limpieza después de su uso (como cerrar un archivo), el provider puede devolver un closure para realizar tal operación. El injector no llamará a esta función de cleanup, cuándo llamarla se deja al llamador del injector, como sigue.

go
type Data struct {
  // TODO cliente de base de datos envuelto
}

// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
  cleanup := func() {
    log.NewHelper(logger).Info("cerrando los recursos de datos")
  }
  return &Data{}, cleanup, nil
}

El código generado realmente puede ser como el siguiente

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
    }
    // inyectar datos
    // ...
    return app, func() {
       cleanup()
    }, nil
}

Tipos duplicados

Es mejor que los parámetros de entrada de provider no tengan tipos duplicados, especialmente para algunos tipos básicos

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

En este caso, la generación de código reportará un error

provider has multiple parameters of type string

wire no podrá distinguir cómo se deben inyectar estos parámetros. Para evitar conflictos, se pueden usar alias de tipos.

Golang editado por www.golangdev.cn