From 31bae0df4e2b8b4fa6d1b4c9a9662d1e3a15f3f1 Mon Sep 17 00:00:00 2001 From: Ross Light Date: Fri, 30 Nov 2018 09:46:48 -0800 Subject: [PATCH] Copy over Wire tutorial from Go Cloud (#86) Originally authored by @enocom. Added to CONTRIBUTORS. Fixes #80 --- CONTRIBUTORS | 1 + README.md | 2 + _tutorial/README.md | 419 ++++++++++++++++++++++++++++++++++++++++++ _tutorial/main.go | 83 +++++++++ _tutorial/wire.go | 27 +++ _tutorial/wire_gen.go | 18 ++ 6 files changed, 550 insertions(+) create mode 100644 _tutorial/README.md create mode 100644 _tutorial/main.go create mode 100644 _tutorial/wire.go create mode 100644 _tutorial/wire_gen.go diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 7015689..2b09493 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -31,6 +31,7 @@ Chris Lewis Christina Austin <4240737+clausti@users.noreply.github.com> +Eno Compton Issac Trotts ktr Oleg Kovalov diff --git a/README.md b/README.md index 4c3597f..9f03e83 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ and ensuring that `$GOPATH/bin` is added to your `$PATH`. ## Documentation +- [Tutorial][] - [User Guide][] - [Best Practices][] - [FAQ][] +[Tutorial]: ./_tutorial/README.md [Best Practices]: ./docs/best-practices.md [FAQ]: ./docs/faq.md [User Guide]: ./docs/guide.md diff --git a/_tutorial/README.md b/_tutorial/README.md new file mode 100644 index 0000000..6967049 --- /dev/null +++ b/_tutorial/README.md @@ -0,0 +1,419 @@ +# Wire Tutorial + +Let's learn to use Wire by example. The [Wire guide][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][guestbook] 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. + +[guestbook]: https://github.com/google/go-cloud/tree/master/samples/guestbook +[guide]: https://github.com/google/wire/blob/master/docs/guide.md + +## 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: + +``` go +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: + +``` go +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. + +``` go +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`: + +``` go +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. + +``` go +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`: + +``` go +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: + +``` go +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][di] 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. + +[di]: https://stackoverflow.com/questions/130794/what-is-dependency-injection + +## 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: + +``` go +func main() { + e := InitializeEvent() + + e.Start() +} +``` + +Next, in a separate file called `wire.go` we will define `InitializeEvent`. +This is where things get interesting: + +``` go +// 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: + +``` go +//+build wireinject + +``` + +Note, a [build constraint][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: + +``` shell +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: + +``` go +// 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. + +[constraint]: https://godoc.org/go/build#hdr-Build_Constraints + +## 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. + +``` go +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: + +``` go +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: + +``` go +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: + +``` go +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: + +``` go +// 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: + +``` go +// 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: + +``` go +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`: + +``` go +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! + +``` go +// 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: + +``` go +func InitializeEvent(phrase string) (Event, error) { + wire.Build(NewEvent, NewMessage) // woops! We to add a provider for Greeter + return Event{}, nil +} +``` + +Running `wire`, we see the following: + +``` shell +# 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`? + +``` go +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: + +``` shell +$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][sets]. +There is support for [binding interfaces][interfaces], [binding +values][values], as well as support for [cleanup functions][cleanup]. See the +[Advanced Features][advanced] section for more. + +[advanced]: https://github.com/google/wire/blob/master/docs/guide.md#advanced-features +[cleanup]: https://github.com/google/wire/blob/master/docs/guide.md#cleanup-functions +[interfaces]: https://github.com/google/wire/blob/master/docs/guide.md#binding-interfaces +[sets]: https://github.com/google/wire/blob/master/docs/guide.md#defining-providers +[values]: https://github.com/google/wire/blob/master/docs/guide.md#binding-values diff --git a/_tutorial/main.go b/_tutorial/main.go new file mode 100644 index 0000000..b00e5b8 --- /dev/null +++ b/_tutorial/main.go @@ -0,0 +1,83 @@ +// Copyright 2018 The Wire Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The greeter binary simulates an event with greeters greeting guests. +package main + +import ( + "errors" + "fmt" + "os" + "time" +) + +// Message is what greeters will use to greet guests. +type Message string + +// NewMessage creates a default Message. +func NewMessage(phrase string) Message { + return Message(phrase) +} + +// NewGreeter initializes a Greeter. If the current epoch time is an even +// number, NewGreeter will create a grumpy Greeter. +func NewGreeter(m Message) Greeter { + var grumpy bool + if time.Now().Unix()%2 == 0 { + grumpy = true + } + return Greeter{Message: m, Grumpy: grumpy} +} + +// Greeter is the type charged with greeting guests. +type Greeter struct { + Grumpy bool + Message Message +} + +// Greet produces a greeting for guests. +func (g Greeter) Greet() Message { + if g.Grumpy { + return Message("Go away!") + } + return g.Message +} + +// NewEvent creates an event with the specified greeter. +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 +} + +// Event is a gathering with greeters. +type Event struct { + Greeter Greeter +} + +// Start ensures the event starts with greeting all guests. +func (e Event) Start() { + msg := e.Greeter.Greet() + fmt.Println(msg) +} + +func main() { + e, err := InitializeEvent("hi there!") + if err != nil { + fmt.Printf("failed to create event: %s\n", err) + os.Exit(2) + } + e.Start() +} diff --git a/_tutorial/wire.go b/_tutorial/wire.go new file mode 100644 index 0000000..a0e0571 --- /dev/null +++ b/_tutorial/wire.go @@ -0,0 +1,27 @@ +// Copyright 2018 The Wire Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//+build wireinject + +// The build tag makes sure the stub is not built in the final build. +package main + +import "github.com/google/wire" + +// InitializeEvent creates an Event. It will error if the Event is staffed with +// a grumpy greeter. +func InitializeEvent(phrase string) (Event, error) { + wire.Build(NewEvent, NewGreeter, NewMessage) + return Event{}, nil +} diff --git a/_tutorial/wire_gen.go b/_tutorial/wire_gen.go new file mode 100644 index 0000000..77e33b0 --- /dev/null +++ b/_tutorial/wire_gen.go @@ -0,0 +1,18 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate wire +//+build !wireinject + +package main + +// Injectors from wire.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 +}