Skip to content

Wire

wire é uma ferramenta de injeção de dependência de código aberto do Google. O conceito de injeção de dependência é bastante popular no framework Spring do Java. Go também tem algumas bibliotecas de injeção de dependência, como o dig open source da Uber. No entanto, a filosofia de injeção de dependência do wire não é baseada no mecanismo de reflexão da linguagem. Estritamente falando, wire é na verdade um gerador de código. O conceito de injeção de dependência se reflete apenas no uso. Se houver problemas, eles podem ser identificados durante a geração do código.

Endereço do repositório: google/wire: Compile-time Dependency Injection for Go (github.com)

Documentação: wire/docs/guide.md at main · google/wire (github.com)

Instalação

Instale a ferramenta de geração de código

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

Instale a dependência do código fonte

go get github.com/google/wire

Introdução

A injeção de dependência no wire é baseada em dois elementos: provider e injector.

provider pode ser um construtor fornecido pelo desenvolvedor, como abaixo. O Provider deve ser exportado.

go
package foobarbaz

type Foo struct {
    X int
}

// Constrói Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

Com parâmetros

go
package foobarbaz

// ...

type Bar struct {
    X int
}

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

Também pode ter parâmetros e valor de retorno

go
package foobarbaz

import (
    "context"
    "errors"
)

type Baz struct {
    X int
}

// ProvideBaz retorna um valor se Bar não for zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("não é possível fornecer baz quando bar é zero")
    }
    return Baz{X: bar.X}, nil
}

Também é possível combinar providers

go
package foobarbaz

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

// ...

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

TIP

wire tem as seguintes regras para valores de retorno do provider:

  • O primeiro valor de retorno é o valor fornecido pelo provider
  • O segundo valor de retorno deve ser func() | error
  • O terceiro valor de retorno, se o segundo valor de retorno for func, então o terceiro valor de retorno deve ser error

injector é uma função gerada pelo wire, responsável por chamar os providers na ordem especificada. A assinatura do injector é definida pelo desenvolvedor, e o wire gera o corpo da função. É declarado chamando wire.Build. Esta declaração não deve ser chamada, muito menos compilada.

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +build wireinject
// A tag de build garante que o stub não seja construído no build 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
}

Então execute

wire

Isso gerará wire_gen.go, com o seguinte conteúdo:

go
// Código gerado pelo Wire. NÃO EDITE.

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

package main

import (
    "example.com/foobarbaz"
)

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

O código gerado tem quase nenhuma dependência do wire, pode funcionar normalmente sem wire, e pode ser gerado novamente executando go generate posteriormente. Depois, o desenvolvedor chama o injector realmente gerado, passando os parâmetros correspondentes para completar a injeção de dependência. Todo o processo do código é bastante simples, parece que é apenas fornecer alguns construtores, depois gerar uma função que chama os construtores, e finalmente chamar esta função passando parâmetros. Parece que não fez nada particularmente complexo, poderia ser feito manualmente também. É exatamente assim, wire faz algo tão simples, apenas transformado de manual para automático. De acordo com a filosofia do wire, injeção de dependência deveria ser algo tão simples, não deveria ser complexificado.

Exemplo

A seguir, um caso para aprofundar o entendimento. Este é um exemplo de inicialização de app.

O provider HttpServer recebe um parâmetro net.Addr e retorna um ponteiro e error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

Os providers MysqlClient e System abaixo são 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
}

Após definir os providers, é necessário definir o injector. É melhor criar um novo arquivo wire.go para definir:

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

package main

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

// define injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
  // chama providers na ordem
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject é para ignorar este injector durante a compilação. Então execute o seguinte comando. Se a saída for como abaixo, a geração foi bem-sucedida.

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

O código gerado é o seguinte:

go
// Código gerado pelo Wire. NÃO EDITE.

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

package main

import (
  "net"
)

// Injectors de wire.go:

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

A lógica é muito clara, e a ordem de chamada também está correta. Finalmente, chame o injector gerado para iniciar o 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()
}

A saída final é:

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

Este é um caso de uso muito simples.

Uso Avançado

Vinculação de Interface

Às vezes, durante a injeção de dependência, uma implementação concreta é injetada em uma interface. No wire, a injeção de dependência é baseada na correspondência de tipos.

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

O parâmetro do provider provideBar é um tipo de interface, mas na verdade é *MyFooer. Para que o provider possa corresponder corretamente durante a geração de código, podemos vincular os dois tipos:

O primeiro parâmetro é o tipo de ponteiro de interface concreto, o segundo é o tipo de ponteiro de implementação concreta.

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

Vinculação de Valor

Ao usar wire.Build, não é necessário que um provider forneça o valor. Também é possível usar wire.Value para fornecer um valor concreto. wire.Value suporta expressões para construir valores. Esta expressão será copiada para o injector durante a geração de código, como abaixo.

go
type Foo struct {
    X int
}

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

O injector gerado:

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

var (
    _wireFooValue = Foo{X: 42}
)

Se quiser vincular um valor de tipo de interface, pode usar wire.InterfaceValue

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

Construção de Estrutura

Em providersets, é possível usar wire.Struct para construir uma estrutura de tipo especificado usando valores de retorno de outros providers.

O primeiro parâmetro deve ser o tipo de ponteiro da estrutura, seguido pelos nomes dos campos.

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

Exemplo:

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

O injector gerado pode ser assim:

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

Se quiser preencher todos os campos, pode usar *, por exemplo:

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

Por padrão, constrói o tipo de estrutura. Se quiser construir o tipo de ponteiro, pode modificar o valor de retorno da assinatura do injector:

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

Se quiser ignorar um campo, pode adicionar uma tag, como abaixo:

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

Cleanup

Se um valor construído por um provider precisar de trabalho de finalização após o uso (como fechar um arquivo), o provider pode retornar um closure para realizar tal operação. O injector não chama esta função cleanup. Quando chamar fica a cargo de quem chama o injector, como abaixo.

go
type Data struct {
  // TODO cliente de banco de dados wrapped
}

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

O código realmente gerado pode ser assim:

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

Tipos Duplicados

É melhor que os parâmetros de entrada do provider não tenham tipos duplicados, especialmente para 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))
}

Neste caso, a geração de código reportará erro:

provider has multiple parameters of type string

O wire não conseguirá distinguir como injetar estes parâmetros. Para evitar conflitos, é possível usar aliases de tipo.

Golang por www.golangdev.cn edit