Wire
wire — это инструмент внедрения зависимостей с открытым исходным кодом от Google. Внедрение зависимостей — это концепция, популяризированная фреймворком Spring для Java. В Go также есть некоторые библиотеки внедрения зависимостей, например, dig от Uber. Однако подход wire к внедрению зависимостей не основан на механизмах отражения языка. Строго говоря, wire на самом деле является генератором кода, а концепция внедрения зависимостей проявляется только в его использовании. Если есть проблемы, они могут быть обнаружены во время генерации кода.
Репозиторий: google/wire: Compile-time Dependency Injection for Go (github.com)
Документация: wire/docs/guide.md at main · google/wire (github.com)
Установка
Установите инструмент генерации кода
go install github.com/google/wire/cmd/wire@latestУстановите зависимости исходного кода
go get github.com/google/wireНачало работы
Внедрение зависимостей в wire основано на двух элементах: provider (поставщик) и injector (инжектор).
provider может быть конструктором, предоставленным разработчиком, как показано ниже. Provider должен быть экспортирован.
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo создаёт Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}С параметрами
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar возвращает Bar: отрицательный Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}Может также иметь параметры и возвращаемые значения
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
// ProvideBaz возвращает значение, если Bar не нулевой.
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
}Providers также могут быть скомбинированы
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire имеет следующие правила для возвращаемых значений provider:
- Первое возвращаемое значение — это значение, предоставленное provider
- Второе возвращаемое значение должно быть
func() | error - Третье возвращаемое значение, если второе возвращаемое значение —
func, то третье должно бытьerror
injector — это функция, сгенерированная wire, которая отвечает за вызов providers в указанном порядке. Сигнатура injector определяется разработчиком, а wire генерирует конкретное тело функции. Это объявляется вызовом wire.Build, и это объявление не должно вызываться, не говоря уже о компиляции.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +build wireinject
// Build-тег гарантирует, что заглушка не будет построена в финальной сборке.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// Определить injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}Затем выполните
wireЭто сгенерирует wire_gen.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
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
}Сгенерированный код практически не имеет зависимостей от wire и может нормально работать без wire. Его можно перегенерировать выполнением go generate впоследствии. После этого разработчики завершают внедрение зависимостей вызовом фактического сгенерированного injector с соответствующими параметрами. Разве весь процесс не довольно прост? Кажется, что нужно просто предоставить несколько конструкторов, затем сгенерировать функцию, которая вызывает конструкторы, и наконец вызвать эту функцию с параметрами. Не похоже, что делается что-то особенно сложное, и вы могли бы написать это вручную. Да, именно это и делает wire — такая простая вещь, просто автоматизированная вместо рукописной. Согласно философии wire, внедрение зависимостей должно быть такой простой вещью и не должно быть сложным.
Пример
Вот случай для углубления понимания. Это пример инициализации приложения.
Provider HttpServer получает параметр net.Addr и возвращает указатель и error
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}Следующие providers MysqlClient и System аналогичны
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
}После определения providers нужно определить injectors. Лучше создать новый файл wire.go для определения их
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// Определить injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// Вызывать providers в порядке
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject нужен для игнорирования этого injector при компиляции. Затем выполните следующую команду. Если вывод следующий, генерация успешна.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.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
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
}Можно видеть, что логика очень ясна, и порядок вызова также корректен. Наконец, запустите приложение вызовом сгенерированного injector.
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()
}Финальный вывод следующий
2023/08/01 19:20:48 app run on 127.0.0.1:8080Это очень простой пример использования.
Продвинутое использование
Привязка интерфейсов
Иногда во время внедрения зависимостей конкретная реализация внедряется в интерфейс. wire сопоставляет зависимости на основе типа во время внедрения зависимостей.
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 будет *MyFooer.
return f.Foo()
}Параметр provider provideBar — это тип интерфейса, но на самом деле это *MyFooer. Чтобы provider правильно сопоставлялся во время генерации кода, мы можем связать два типа вместе, как follows:
Первый параметр — это тип конкретного интерфейса, второй — тип конкретной реализации.
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)Привязка значений
При использовании wire.Build не обязательно использовать provider для предоставления значения. Можно также использовать wire.Value для предоставления конкретного значения. wire.Value поддерживает выражения для конструирования значений, и это выражение будет скопировано в injector при генерации кода, как follows.
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}Сгенерированный injector
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)Если хотите привязать значение типа интерфейса, можно использовать wire.InterfaceValue
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}Конструирование структур
В providerset можно использовать wire.Struct для конструирования структуры указанного типа с использованием возвращаемых значений от других providers.
Первый параметр должен быть типом указателя на структуру, далее — имена полей.
func Struct(structType interface{}, fieldNames ...string) StructProviderПример:
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)
}Сгенерированный injector может выглядеть так
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}Если хотите заполнить все поля, можно использовать *, например
wire.Struct(new(FooBar), "*")По умолчанию конструируется тип структуры. Если хотите конструировать тип указателя, можно изменить возвращаемое значение сигнатуры injector
func injectFooBar() *FoodBar {
wire.Build(Set)
}Если хотите игнорировать поле, можно добавить тег, как follows
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Очистка
Если значение, сконструированное provider, требует работы по очистке после использования (например, закрытие файла), provider может вернуть замыкание для выполнения таких операций. Injector не будет вызывать эту функцию очистки; когда вызывать её, оставляется на усмотрение вызывающего injector, как follows.
type Data struct {
// TODO обёрнутый клиент базы данных
}
// 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
}Фактический сгенерированный код может выглядеть так
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
}Дублирование типов
Лучше не иметь дублирующихся типов в параметрах provider, особенно для базовых типов
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))
}В этом случае генерация кода сообщит об ошибке
provider has multiple parameters of type stringwire не сможет различить, как эти параметры должны быть внедрены. Чтобы избежать конфликтов, можно использовать псевдонимы типов.
