Skip to content

Wire

wire is a dependency injection tool open-sourced by Google. Dependency injection is a concept popularized by Java's Spring framework. Go also has some dependency injection libraries, such as Uber's open-source dig. However, wire's dependency injection approach is not based on language reflection mechanisms. Strictly speaking, wire is actually a code generator, and the dependency injection concept is only reflected in its usage. If there are problems, they can be found during code generation.

Repository: google/wire: Compile-time Dependency Injection for Go (github.com)

Documentation: wire/docs/guide.md at main · google/wire (github.com)

Installation

Install the code generation tool

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

Install source code dependencies

go get github.com/google/wire

Getting Started

Dependency injection in wire is based on two elements: provider and injector.

provider can be a constructor provided by the developer, as shown below. The Provider must be exported.

go
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo constructs a Foo
func ProvideFoo() Foo {
    return Foo{X: 42}
}

With parameters

go
package foobarbaz

// ...

type Bar struct {
    X int
}

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

Can also have parameters and return values

go
package foobarbaz

import (
    "context"
    "errors"
)

type Baz struct {
    X int
}

// ProvideBaz returns a value if Bar is not zero.
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 can also be combined

go
package foobarbaz

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

// ...

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

TIP

wire has the following rules for provider return values:

  • The first return value is the value provided by the provider
  • The second return value must be func() | error
  • The third return value, if the second return value is func, then the third return value must be error

injector is a function generated by wire that is responsible for calling providers in the specified order. The injector's signature is defined by the developer, and wire generates the specific function body. This is declared by calling wire.Build, and this declaration should not be called, let alone compiled.

go
func Build(...interface{}) string {
  return "implementation not generated, run wire"
}
go
// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

// Define injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}

Then execute

wire

This will generate wire_gen.go with the following content:

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

// Actual generated 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
}

The generated code has almost no dependencies on wire, and can work normally without wire. It can be regenerated by executing go generate subsequently. After that, developers complete dependency injection by calling the actual generated injector with the corresponding parameters. Isn't the whole process quite simple? It feels like just providing a few constructors, then generating a function that calls the constructors, and finally calling this function with parameters. It doesn't seem to do anything particularly complex, and you could write it by hand. Yes, that's exactly what wire does - it's such a simple thing, just automated instead of handwritten. According to wire's philosophy, dependency injection should be such a simple thing and should not be complicated.

Example

Here's a case to deepen understanding. This is an example of initializing an app.

HttpServer's provider receives a net.Addr parameter and returns a pointer and error

go
var ServerProviderSet = wire.NewSet(NewHttpserver)

type HttpServer struct {
  net.Addr
}

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

The following MysqlClient and System providers are similar

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
}

After defining providers, you need to define injectors. It's best to create a new wire.go file to define them

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) {
  // Call providers in order
  panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}

+build wireinject is to ignore this injector during compilation. Then execute the following command. If the output is as follows, the generation is successful.

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

The generated code is as follows

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:

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

You can see the logic is very clear, and the call order is also correct. Finally, start the app by calling the generated 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()
}

The final output is as follows

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

This is a very simple usage example.

Advanced Usage

Interface Binding

Sometimes, during dependency injection, a concrete implementation is injected into an interface. wire matches dependencies based on type during dependency injection.

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 will be a *MyFooer.
    return f.Foo()
}

The parameter of provider provideBar is an interface type, but it's actually *MyFooer. To let the provider match correctly during code generation, we can bind the two types together, as follows:

The first parameter is the concrete interface pointer type, and the second is the concrete implementation pointer type.

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

Value Binding

When using wire.Build, you don't have to use a provider to provide a value. You can also use wire.Value to provide a concrete value. wire.Value supports expressions to construct values, and this expression will be copied into the injector when generating code, as follows.

go
type Foo struct {
    X int
}

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

Generated injector

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

var (
    _wireFooValue = Foo{X: 42}
)

If you want to bind an interface type value, you can use wire.InterfaceValue

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

Struct Construction

In a providerset, you can use wire.Struct to construct a struct of a specified type using return values from other providers.

The first parameter should be a struct pointer type, followed by field names.

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

Example:

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

The generated injector may look like this

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

If you want to fill all fields, you can use *, for example

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

By default, it constructs a struct type. If you want to construct a pointer type, you can modify the injector signature's return value

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

If you want to ignore a field, you can add a tag, as follows

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

Cleanup

If a value constructed by a provider needs cleanup work after use (such as closing a file), the provider can return a closure to perform such operations. The injector will not call this cleanup function; when to call it is left to the injector's caller, as follows.

go
type Data struct {
  // TODO wrapped database client
}

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

The actual generated code may look like this

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
}

Type Duplication

It's best not to have duplicate types in provider parameters, especially for basic types

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

In this case, code generation will report an error

provider has multiple parameters of type string

wire will not be able to distinguish how these parameters should be injected. To avoid conflicts, you can use type aliases.

Golang by www.golangdev.cn edit