Skip to content

Files

Latest commit

737d461 · Aug 30, 2021

History

History
This branch is 2 commits ahead of, 5 commits behind google/wire:main.

_tutorial

Wire Tutorial

Let's learn to use Wire by example. The Wire guide provides thorough documentation of the tool's usage. For readers eager to see Wire applied to a larger server, the guestbook sample in Go Cloud uses Wire to initialize its components. Here we are going to build a small greeter program to understand how to use Wire. The finished product may be found in the same directory as this README.

A First Pass of Building the Greeter Program

Let's create a small program that simulates an event with a greeter greeting guests with a particular message.

To start, we will create three types: 1) a message for a greeter, 2) a greeter who conveys that message, and 3) an event that starts with the greeter greeting guests. In this design, we have three struct types:

type Message string

type Greeter struct {
    // ... TBD
}

type Event struct {
    // ... TBD
}

The Message type just wraps a string. For now, we will create a simple initializer that always returns a hard-coded message:

func NewMessage() Message {
    return Message("Hi there!")
}

Our Greeter will need reference to the Message. So let's create an initializer for our Greeter as well.

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

type Greeter struct {
    Message Message // <- adding a Message field
}

In the initializer we assign a Message field to Greeter. Now, we can use the Message when we create a Greet method on Greeter:

func (g Greeter) Greet() Message {
    return g.Message
}

Next, we need our Event to have a Greeter, so we will create an initializer for it as well.

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

type Event struct {
    Greeter Greeter // <- adding a Greeter field
}

Then we add a method to start the Event:

func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

The Start method holds the core of our small application: it tells the greeter to issue a greeting and then prints that message to the screen.

Now that we have all the components of our application ready, let's see what it takes to initialize all the components without using Wire. Our main function would look like this:

func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}

First we create a message, then we create a greeter with that message, and finally we create an event with that greeter. With all the initialization done, we're ready to start our event.

We are using the dependency injection design principle. In practice, that means we pass in whatever each component needs. This style of design lends itself to writing easily tested code and makes it easy to swap out one dependency with another.

Using Wire to Generate Code

One downside to dependency injection is the need for so many initialization steps. Let's see how we can use Wire to make the process of initializing our components smoother.

Let's start by changing our main function to look like this:

func main() {
    e := InitializeEvent()

    e.Start()
}

Next, in a separate file called wire.go we will define InitializeEvent. This is where things get interesting:

// wire.go

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

Rather than go through the trouble of initializing each component in turn and passing it into the next one, we instead have a single call to wire.Build passing in the initializers we want to use. In Wire, initializers are known as "providers," functions which provide a particular type. We add a zero value for Event as a return value to satisfy the compiler. Note that even if we add values to Event, Wire will ignore them. In fact, the injector's purpose is to provide information about which providers to use to construct an Event and so we will exclude it from our final binary with a build constraint at the top of the file:

//+build wireinject

Note, a build constraint requires a blank, trailing line.

In Wire parlance, InitializeEvent is an "injector." Now that we have our injector complete, we are ready to use the wire command line tool.

Install the tool with:

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

Then in the same directory with the above code, simply run wire. Wire will find the InitializeEvent injector and generate a function whose body is filled out with all the necessary initialization steps. The result will be written to a file named wire_gen.go.

Let's take a look at what Wire did for us:

// wire_gen.go

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

It looks just like what we wrote above! Now this is a simple example with just three components, so writing the initializer by hand isn't too painful. Imagine how useful Wire is for components that are much more complex. When working with Wire, we will commit both wire.go and wire_gen.go to source control.

Making Changes with Wire

To show a small part of how Wire handles more complex setups, let's refactor our initializer for Event to return an error and see what happens.

func NewEvent(g Greeter) (Event, error) {
    if g.Grumpy {
        return Event{}, errors.New("could not create event: event greeter is grumpy")
    }
    return Event{Greeter: g}, nil
}

We'll say that sometimes a Greeter might be grumpy and so we cannot create an Event. The NewGreeter initializer now looks like this:

func NewGreeter(m Message) Greeter {
    var grumpy bool
    if time.Now().Unix()%2 == 0 {
        grumpy = true
    }
    return Greeter{Message: m, Grumpy: grumpy}
}

We have added a Grumpy field to Greeter struct and if the invocation time of the initializer is an even number of seconds since the Unix epoch, we will create a grumpy greeter instead of a friendly one.

The Greet method then becomes:

func (g Greeter) Greet() Message {
    if g.Grumpy {
        return Message("Go away!")
    }
    return g.Message
}

Now you see how a grumpy Greeter is no good for an Event. So NewEvent may fail. Our main must now take into account that InitializeEvent may in fact fail:

func main() {
    e, err := InitializeEvent()
    if err != nil {
        fmt.Printf("failed to create event: %s\n", err)
        os.Exit(2)
    }
    e.Start()
}

We also need to update InitializeEvent to add an error type to the return value:

// wire.go

func InitializeEvent() (Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}, nil
}

With the setup complete, we are ready to invoke the wire command again. Note, that after running wire once to produce a wire_gen.go file, we may also use go generate. Having run the command, our wire_gen.go file looks like this:

// wire_gen.go

func InitializeEvent() (Event, error) {
    message := NewMessage()
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

Wire has detected that the NewEvent provider may fail and has done the right thing inside the generated code: it checks the error and returns early if one is present.

Changing the Injector Signature

As another improvement, let's look at how Wire generates code based on the signature of the injector. Presently, we have hard-coded the message inside NewMessage. In practice, it's much nicer to allow callers to change that message however they see fit. So let's change InitializeEvent to look like this:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}, nil
}

Now InitializeEvent allows callers to pass in the phrase for a Greeter to use. We also add a phrase argument to NewMessage:

func NewMessage(phrase string) Message {
    return Message(phrase)
}

After we run wire again, we will see that the tool has generated an initializer which passes the phrase value as a Message into Greeter. Neat!

// wire_gen.go

func InitializeEvent(phrase string) (Event, error) {
    message := NewMessage(phrase)
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

Wire inspects the arguments to the injector, sees that we added a string to the list of arguments (e.g., phrase), and likewise sees that among all the providers, NewMessage takes a string, and so it passes phrase into NewMessage.

Catching Mistakes with Helpful Errors

Let's also look at what happens when Wire detects mistakes in our code and see how Wire's error messages help us correct any problems.

For example, when writing our injector InitializeEvent, let's say we forget to add a provider for Greeter. Let's see what happens:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewMessage) // woops! We forgot to add a provider for Greeter
    return Event{}, nil
}

Running wire, we see the following:

# wrapping the error across lines for readability
$GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1:
inject InitializeEvent: no provider found for github.com/google/wire/_tutorial.Greeter
(required by provider of github.com/google/wire/_tutorial.Event)
wire: generate failed

Wire is telling us some useful information: it cannot find a provider for Greeter. Note that the error message prints out the full path to the Greeter type. It's also telling us the line number and injector name where the problem occurred: line 24 inside InitializeEvent. In addition, the error message tells us which provider needs a Greeter. It's the Event type. Once we pass in a provider of Greeter, the problem will be solved.

Alternatively, what happens if we provide one too many providers to wire.Build?

func NewEventNumber() int  {
    return 1
}

func InitializeEvent(phrase string) (Event, error) {
     // woops! NewEventNumber is unused.
    wire.Build(NewEvent, NewGreeter, NewMessage, NewEventNumber)
    return Event{}, nil
}

Wire helpfully tells us that we have an unused provider:

$GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1:
inject InitializeEvent: unused provider "NewEventNumber"
wire: generate failed

Deleting the unused provider from the call to wire.Build resolves the error.

Conclusion

Let's summarize what we have done here. First, we wrote a number of components with corresponding initializers, or providers. Next, we created an injector function, specifying which arguments it receives and which types it returns. Then, we filled in the injector function with a call to wire.Build supplying all necessary providers. Finally, we ran the wire command to generate code that wires up all the different initializers. When we added an argument to the injector and an error return value, running wire again made all the necessary updates to our generated code.

The example here is small, but it demonstrates some of the power of Wire, and how it takes much of the pain out of initializing code using dependency injection. Furthermore, using Wire produced code that looks much like what we would otherwise write. There are no bespoke types that commit a user to Wire. Instead it's just generated code. We may do with it what we will. Finally, another point worth considering is how easy it is to add new dependencies to our component initialization. As long as we tell Wire how to provide (i.e., initialize) a component, we may add that component anywhere in the dependency graph and Wire will handle the rest.

In closing, it is worth mentioning that Wire supports a number of additional features not discussed here. Providers may be grouped in provider sets. There is support for binding interfaces, binding values, as well as support for cleanup functions. See the Advanced Features section for more.