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 install github.com/google/wire/cmd/wire@latestInstale a dependência do código fonte
go get github.com/google/wireIntroduçã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.
package foobarbaz
type Foo struct {
X int
}
// Constrói Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}Com parâmetros
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
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
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 sererror
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.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +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
wireIsso gerará wire_gen.go, com o seguinte conteúdo:
// 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
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
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: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.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goO código gerado é o seguinte:
// 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.
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:8080Este é 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.
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.
func Bind(iface, to interface{}) Bindingvar 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.
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
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.
func Struct(structType interface{}, fieldNames ...string) StructProviderExemplo:
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:
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:
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:
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.
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:
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.
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 stringO wire não conseguirá distinguir como injetar estes parâmetros. Para evitar conflitos, é possível usar aliases de tipo.
