Skip to content

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
go install github.com/google/wire/cmd/wire@latest

Установите зависимости исходного кода

go get github.com/google/wire

Начало работы

Внедрение зависимостей в wire основано на двух элементах: provider (поставщик) и injector (инжектор).

provider может быть конструктором, предоставленным разработчиком, как показано ниже. Provider должен быть экспортирован.

go
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo создаёт Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

С параметрами

go
package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar возвращает Bar: отрицательный Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

Может также иметь параметры и возвращаемые значения

go
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 также могут быть скомбинированы

go
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, и это объявление не должно вызываться, не говоря уже о компиляции.

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +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 со следующим содержимым:

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

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

Следующие providers MysqlClient и System аналогичны

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
}

После определения providers нужно определить injectors. Лучше создать новый файл wire.go для определения их

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 при компиляции. Затем выполните следующую команду. Если вывод следующий, генерация успешна.

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

Сгенерированный код следующий

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.

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

Финальный вывод следующий

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

Это очень простой пример использования.

Продвинутое использование

Привязка интерфейсов

Иногда во время внедрения зависимостей конкретная реализация внедряется в интерфейс. wire сопоставляет зависимости на основе типа во время внедрения зависимостей.

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 будет *MyFooer.
    return f.Foo()
}

Параметр provider provideBar — это тип интерфейса, но на самом деле это *MyFooer. Чтобы provider правильно сопоставлялся во время генерации кода, мы можем связать два типа вместе, как follows:

Первый параметр — это тип конкретного интерфейса, второй — тип конкретной реализации.

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

Привязка значений

При использовании wire.Build не обязательно использовать provider для предоставления значения. Можно также использовать wire.Value для предоставления конкретного значения. wire.Value поддерживает выражения для конструирования значений, и это выражение будет скопировано в injector при генерации кода, как follows.

go
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

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

Конструирование структур

В providerset можно использовать wire.Struct для конструирования структуры указанного типа с использованием возвращаемых значений от других providers.

Первый параметр должен быть типом указателя на структуру, далее — имена полей.

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

Пример:

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

Сгенерированный injector может выглядеть так

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

Если хотите заполнить все поля, можно использовать *, например

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

По умолчанию конструируется тип структуры. Если хотите конструировать тип указателя, можно изменить возвращаемое значение сигнатуры injector

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

Если хотите игнорировать поле, можно добавить тег, как follows

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

Очистка

Если значение, сконструированное provider, требует работы по очистке после использования (например, закрытие файла), provider может вернуть замыкание для выполнения таких операций. Injector не будет вызывать эту функцию очистки; когда вызывать её, оставляется на усмотрение вызывающего injector, как follows.

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

Фактический сгенерированный код может выглядеть так

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

Дублирование типов

Лучше не иметь дублирующихся типов в параметрах provider, особенно для базовых типов

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

В этом случае генерация кода сообщит об ошибке

provider has multiple parameters of type string

wire не сможет различить, как эти параметры должны быть внедрены. Чтобы избежать конфликтов, можно использовать псевдонимы типов.

Golang by www.golangdev.cn edit