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 install github.com/google/wire/cmd/wire@latestInstall source code dependencies
go get github.com/google/wireGetting 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.
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo constructs a Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}With parameters
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
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
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 beerror
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.
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +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
wireThis will generate wire_gen.go with the following content:
// 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
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
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: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.
$ wire
$ wire: golearn: wrote /golearn/wire_gen.goThe generated code is as follows
// 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.
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:8080This 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.
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.
func Bind(iface, to interface{}) Bindingvar 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.
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
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.
func Struct(structType interface{}, fieldNames ...string) StructProviderExample:
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
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
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
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.
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
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
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 stringwire will not be able to distinguish how these parameters should be injected. To avoid conflicts, you can use type aliases.
