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 install github.com/google/wire/cmd/wire@latestInstalar dependencias de código fuente
go get github.com/google/wireIntroducció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.
package foobarbaz
type Foo struct {
X int
}
// Constructor de Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Con parámetros
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
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
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 sererror
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.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +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
wiregenerará wire_gen.go, el contenido es el siguiente
// 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
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
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: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.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goEl código después de la generación es el siguiente
// 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.
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:8080Este 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.
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.
func Bind(iface, to interface{}) Bindingvar 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.
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
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.
func Struct(structType interface{}, fieldNames ...string) StructProviderEjemplo como sigue
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
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
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
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.
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
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
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 stringwire no podrá distinguir cómo se deben inyectar estos parámetros. Para evitar conflictos, se pueden usar alias de tipos.
