avatarChe Dan

Summary

Wire is a dependency injection tool for Golang that generates code at compile time, adhering to Go's philosophy and offering benefits such as easy debugging, no runtime reflection, and better tooling support.

Abstract

Wire is a lightweight and compile-time dependency injection tool developed by the Go Cloud team for the Go programming language. It stands out from other runtime dependency injection solutions by generating source code, thus avoiding the use of reflection or service locators. This approach ensures that dependencies are clear and maintainable, facilitates debugging, and allows for static analysis of the dependency graph. Wire's design aligns with Go's ethos of simplicity and clarity, as reflected in the Go proverb "Clear is better than clever." Despite being a relatively new tool, Wire is considered mature and unlikely to undergo significant changes, focusing instead on bug fixes and stability. The tool supports advanced features such as binding interfaces, struct field injection, and cleanup functions for resource management, demonstrating its comprehensive capabilities in managing dependencies in Go applications.

Opinions

  • The Go Cloud team believes that existing dependency injection solutions in the Golang community do not align with Go's design philosophy, which prioritizes clarity and simplicity over cleverness and runtime reflection.
  • Wire's compile-time code generation is favored over runtime dependency injection due to its ability to provide explicit error messages, avoid dependency bloat, and enable better tooling and visualization.
  • The design of Wire reflects a commitment to the SOLID principles, particularly the Dependency Inversion Principle, by allowing for loose coupling and easy maintenance of software components.
  • The Wire team emphasizes the tool's simplicity and does not plan to add new features, focusing instead on maintaining its current functionality and ensuring reliability.
  • The use of Wire is recommended over manual dependency management when dealing with complex initialization processes involving many components, as it automates the generation of boilerplate code and ensures proper resource cleanup.

Mastering Wire

What is it ?

Wire is a lightweight dependency injection tool in Golang which was developed by the Go Cloud team. It automatically generate code then injection dependency at compile time.

Dependency Injection is one of the most important design principles for keeping software “loose-coupling and easy to maintain”.

This principle is widely used in all kind of development platforms and there are many excellent tools related to it.

Among all these tool , the most famous one is Spring and Spring IOC as the foundation of the framework has played a decisive role to its dominant position today.

In fact, the “D” in the S.O.L.I.D principle of software development refers specifically to this topic.

Why Wire is different ?

Dependency injection is so important, that there are quite a few solutions for this in the Golang community already, such as dig from Uber and inject from Facebook. Both of them implement runtime dependency injection through Reflection Mechanisms.

Why did the Go Cloud team reinvent the wheel? Because in their opinion none of the above libraries conform to Go’s philosophy:

Clear is better than cleverReflection is never clear.

— Rob Pike

As a code generation tool, Wire can generate source code and implement dependency injection at compile time. It does not require Reflection or Service Locators. As you will see later, the codes generated by Wire have no difference from those written by hand. This approach brings some benefits:

  1. Easy debug. If any dependency is missing, an error will be reported during compiling
  2. Since no service locators are needed, there are no special requirements for naming
  3. Avoid dependency bloat. The generated code will only import the dependency you need, while runtime dependency injection cannot identify unused dependencies until runtime.
  4. Dependency graph is stored in source code statically, which make tooling and visualization easier

The detail trade-off of designing Wire can be found on Go Blog.

Although the latest release of Wire is just v0.4.0, it has achieved the goals set by the team and is quite mature . No major changes are expected in the future. This can be seen from the team’s statement:

It works well for the tasks it was designed to perform, and we prefer to keep it as simple as possible.

We’ll not be accepting new features at this time, but will gladly accept bug reports and fixes.

Wire team

Getting Started

Installing wire is quite easy, just run

 go get github.com/google/wire/cmd/wire 

you’ll get wire command line tool installed at $GOPATH/bin, make sure $GOPATH/bin is in $PATH , then you can run wire at any directory.

Before going further, we need to explain two core concepts in Wire: Provider and Injector.

Provider: plain function for creating components. These methods take the required dependencies as parameters, create a component and return it.

A component can be an object or function, in fact it can be of any type. the only limit is: one type can only have a single provider in the entire dependency graph. So a provider returning int is not a good idea. In this case, you can solve it by defining a type alias. For example, first define type Category int and then let the provider return the Category type

Here are the typical provider examples:

// DefaultConnectionOpt provide default connection option
func DefaultConnectionOpt()*ConnectionOpt{...}// NewDb provide an Db object
func NewDb(opt *ConnectionOpt)(*Db, error){...}// NewUserLoadFunc provide a function which can load user
func NewUserLoadFunc(db *Db)(func(int) *User, error){...}

In practice, a group of related providers are often put together and organized into a ProviderSet to facilitate maintenance and switching.

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

Injector: A function generated by wire, that call providers in dependency order.

In order to generate injector, we define the injector function signature in wire.go (file name is not mandatory, but this is generally the case) . Then call wire.Build in the function body with provider as parameter (regardless of the order).

Since the functions in wire.go do not really return a value, in order to avoid compiler errors, simply wrap them with panic functions. Don’t worry about runtime error, because it will not actually execution, it is just the hint for generating real code. A simple wire.go example:

// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

Having this code done, run command wire will generate file wire_gen.go , which holds the actual implementation of the injector function. Any non-injector code in wire.go will be copied to wire_gen.go as is (although technically allowed, this is not recommended). The generated code is as follows:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

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

// Injectors from wire.go:

func UserLoader() (func(int) *User, error) {
   connectionOpt := DefaultConnectionOpt()
   db, err := NewDb(connectionOpt)
   if err != nil {
      return nil, err
   }
   v, err := NewUserLoadFunc(db)
   if err != nil {
      return nil, err
   }
   return v, nil
}

// wire.go:

var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)

There are two interesting points in the above code:

  1. The first line of wire.go // + build wireinject, this build tag ensures that the wire.go file is ignored during regular compilation (because the wireinject tag is not specified during regular compilation). And In contrast, the 4th line of wire_gen.go // + build! Wireinject. These two sets of opposing build tags guarantee that in any case, only one file of wire.go and wire_gen.go takes effect, avoiding compilation errors “function UserLoader has been defined”
  2. The automatically generated function UserLoader contains error handling. It’s almost the same as handwritten code. For such a simple initialization process, handwriting is acceptable, but when the number of components reaches tens, hundreds or even more, the advantages of automatic generation will be showed up.

There are two ways to trigger the “generate” action: go generate or wire.

The former is only valid if wire_gen.go already exists (because the third line of wire_gen.go // go: generate wire).

While the latter can be executed at any time. And the latter supports more arguments to fine-tune the generation behavior, so it is recommended to always use the wire command.

Then we can use the real injector, for example:

package main

import "log"

func main() {
   fn, err := UserLoader()
   if err != nil {
      log.Fatal(err)
   }
   user := fn(123)
   ...
}

If you accidentally forget a certain provider, wire will report specific errors to help developer resolve the problem quickly. For example, we modify wire.go to remove NewDb

// +build wireinject

package main

import "github.com/google/wire"

func UserLoader()(func(int)*User, error){
   panic(wire.Build(NewUserLoadFunc, DbSet))
}

var DbSet = wire.NewSet(DefaultConnectionOpt) //forgot add Db provider

execute wire command, then an explicit error will be reported: “no provider found for * example.Db

wire: /usr/example/wire.go:7:1: inject UserLoader: no provider found for *example.Db
      needed by func(int) *example.User in provider "NewUserLoadFunc" (/usr/example/provider.go:24:6)
wire: example: generate failed
wire: at least one generate failure

Similarly, if unused providers are written in wire.go, there will be explicit error messages.

Advanced Features

After talking about basic usage, let’s take a look at advanced features

Binding Interfaces

Sometimes we need to inject an interface. There are two options for this:

  1. The more straightforward way is to create a class in provider and then return the interface type. But this does not conform to the Go best practice. not recommended
  2. Let the provider return class, then declare an interface binding in injector, for example:
// FooInf, an interface
// FooClass, an class which implements FooInf 
// fooClassProvider, a provider function that provider *FooClassvar 
set = wire.NewSet(
    fooClassProvider, 
    wire.Bind(new(FooInf), new(*FooClass) // bind class to interface
)

Struct Providers

Sometimes we don’t need any specific initialization work, we simply create an struct instance, assign a value to the specified field, and then return. When there are many fields, this kind of work could be tedious.

// provider.go
type App struct {
    Foo *Foo
    Bar *Bar
}func DefaultApp(foo *Foo, bar *Bar)*App{
    return &App{Foo: foo, Bar: bar}
}
// wire.go
...
wire.Build(provideFoo, provideBar, DefaultApp)
...

In this case , wire.Struct comes to rescue, injecting fields by specifying field names:

wire.Build(provideFoo, provideBar, wire.Struct(new(App),"Foo","Bar")

If you want to inject all fields, there is a more simplified way:

wire.Build(provideFoo, provideBar, wire.Struct(new(App), "*")

If you want to ignore some fields in the struct, then you can modify the struct definition:

type App struct {
    Foo *Foo
    Bar *Bar
    NoInject int `wire:"-"`
}

Then NoInject will be ignored. Compared to regular providers, wire.Struct provides an additional flexibility: it can adapt to pointer and non-pointer types and automatically adjust the generated code as needed.

While wire.Struct does provide some convenience. But it requires injected fields to be publicly accessible, which causes struct to expose details that could otherwise be hidden.

Fortunately, this problem can be solved by “binding interface” mentioned above. Construct an object with wire.Struct and bind class to the interface. As for how to make the trade-off between convenience and encapsulation, it depends on your specific situation.

Binding Values

Occasionally you need to bind basic values to a field. In this case, you can use wire.Value:

// provider.go
type Foo struct {
    X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...

For interface values, use wire.InterfaceValue

wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))

Use Fields of a Struct as Providers

Sometimes we need to use field of a struct as Provider, for example:

// provider
func provideBar(foo Foo)*Bar{
    return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...

In this case , you can use wire.FieldsOf to simplify it, avoid tedious providers definition :

wire.Build(provideFoo, wire.FieldsOf(new(Foo), “Bar”))

Similar to wire.Struct, wire.FieldsOf also automatically adapts to pointer / non-pointer injection requests

Cleanup functions

As mentioned earlier, if provider and injector functions return errors, Wire will automatically handle them. In addition, Wire has another automatic processing capability: cleanup functions.

The so-called cleanup function refers to a closure with a signature func(). It returns from provider to ensure that the resources allocated in provider can be cleaned up.

The typical application scenarios of the cleanup function are file resources and network connection resources management, for example:

type App struct {
   File *os.File
   Conn net.Conn
}

func provideFile() (*os.File, func(), error) {
   f, err := os.Open("foo.txt")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := f.Close(); err != nil {
         log.Println(err)
      }
   }
   return f, cleanup, nil
}

func provideNetConn() (net.Conn, func(), error) {
   conn, err := net.Dial("tcp", "foo.com:80")
   if err != nil {
      return nil, nil, err
   }
   cleanup := func() {
      if err := conn.Close(); err != nil {
         log.Println(err)
      }
   }
   return conn, cleanup, nil
}

The above codes define two providers that provide file resources and network connection resources, respectively.

wire.go

// +build wireinject

package main

import "github.com/google/wire"

func NewApp() (*App, func(), error) {
   panic(wire.Build(
      provideFile,
      provideNetConn,
      wire.Struct(new(App), "*"),
   ))
}

Note that because providers return a cleanup function, the injector function must also return it, otherwise an error will occur

wire_gen.go

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func NewApp() (*App, func(), error) {
   file, cleanup, err := provideFile()
   if err != nil {
      return nil, nil, err
   }
   conn, cleanup2, err := provideNetConn()
   if err != nil {
      cleanup()
      return nil, nil, err
   }
   app := &App{
      File: file,
      Conn: conn,
   }
   return app, func() {
      cleanup2()
      cleanup()
   }, nil
}

There are two things worth noting in the generated code:

  1. cleanup() is called when provideNetConn fails, which ensures that subsequent processing errors will not affect the cleanup of previously allocated resources.
  2. The final returned closure automatically combines cleanup2() and cleanup(). This means that no matter how many resources are allocated, as long as the calling process is successful, their cleanup work will be handled in one cleanup function. The caller of the injector will be responsible for the final cleanup

It can be imagined that when dozens of cleaning functions are combined together, manually processing the above two issues is very tedious and error-prone. The advantages of Wire are showed up once again.

Then we can use it:

func main() {
   app, cleanup, err := NewApp()
   if err != nil {
      log.Fatal(err)
   }
   defer cleanup()
   ...
}

Note defer cleanup() , which ensures that all resources are eventually recycled.

Summary

In this article , we introduced the concept, basic usage and various advanced features of Wire in detail. Hope that can help you master this tiny yet powerful tool.

p.s. Happy New Year !

Golang
Go
Dependency Injection
Recommended from ReadMedium