Google Wire:Go中的依赖注入框架
- Wire 指南
- 建立Greeter Program的第一步
- 用 Wire 生成代码
- 使用Wire进行更改
- 更改Injector的签名
- 用Errors来捕捉异常
- 结论
Wire 指南
下面我们通过example的形式来学习如何使用Wire。《Wire指南》提供了关于工具使用的详细文档。对于希望将Wire应用于大型服务器的读者,“Go Cloud用户示例”使用Wire来初始化组件。这里我们通过构建一个小greeter program,来了解如何使用Wire。 在 README所在的目录中可以找到最终产品。
建立Greeter Program的第一步
先创建一个小项目,来模拟greeter向访客发送消息的事件。
首先,我们将创建三种类型:1)给greeter的消息,2)传达该消息的greeter,以及3)以greeter打招呼开始的event。 在此设计中,我们有三种struct类型:
1 2 3 4 5 6 7 | type Message string type Greeter struct { Message Message } type Event struct { Greeter Greeter } |
Message 类型仅包装了一个string,创建一个简单的构造函数,返回消息"Hi there!":
1 2 3 | func NewMessage() Message { return Message("Hi there!") } |
Greeter需要依赖message,所以也需要为Greeter创建一个构造函数
1 2 3 4 5 6 | func NewGreeter(m Message) Greeter { return Greeter{Message: m} } type Greeter struct { Message Message // <- adding a Message field } |
在构造函数中,我们为Greeter分配一个Message字段。现在,当我们在Greeter上创建Greet方法时,可以使用Message:
1 2 3 | func (g Greeter) Greet() Message { return g.Message } |
接下来,Event需要一个Greeter,因此我们也为其创建一个初始化程序。
1 2 3 4 5 6 | func NewEvent(g Greeter) Event { return Event{Greeter: g} } type Event struct { Greeter Greeter // <- adding a Greeter field } |
然后添加一个启动event的方法:
1 2 3 4 | func (e Event) Start() { msg := e.Greeter.Greet() fmt.Println(msg) } |
启动方法是应用程序的核心:它告诉greeter发出问候并打印。
现在程序的所有组件都已准备完毕,下面让我们看看不使用Wire初始化所有组件需要做什么。 我们的主要功能如下所示:
1 2 3 4 5 6 7 | func main() { message := NewMessage() greeter := NewGreeter(message) event := NewEvent(greeter) event.Start() } |
首先,创建一条消息,然后创建一个使用该消息的greeter,最后我们使用该greeter创建一个event。完成所有初始化后就可以开始事件了。
我们正在使用“依赖注入”设计原理。 实际上,这意味着组件所需的任何内容都会被传递。这种设计风格让代码的编写和测试变得更加简单,并可以轻松地将一个依赖关系换成另一个。
用 Wire 生成代码
依赖注入的一个缺点是需要很多初始化步骤。 下面看看如何让使用Wire初始化过程更流畅。
让我们先将主要功能更改为如下所示:
1 2 3 4 5 | func main() { e := InitializeEvent() e.Start() } |
接下来,在一个单独的名为wire.go的文件中,定义InitializeEvent。 事情会变得有趣:
1 2 3 4 5 | // wire.go func InitializeEvent() Event { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{} } |
相比依次初始化每个组件并传递到下一个组件,现在只需要一次调用wire.Build即可。 在Wire中,初始化程序称为“providers”,可以提供特定类型的功能。我们为Event添加一个零值作为返回值以满足编译器的要求。请注意,即使我们向Event添加值,Wire也会忽略。 实际上,注入器的目的是提供有关构造事件的providers 的信息,因此我们将通过在文件顶部的build constraint,将其从最终的二进制文件中排除:
1 | //+build wireinject |
注意,build constraint要求末尾有空白。
用Wire的话来说,InitializeEvent是一个“injector注入器”。 现在我们已经完成了injector的准备工作,现在可以使用wire命令行工具了。
使用以下工具安装:
1 | go get github.com/google/wire/cmd/wire |
在与上述代码相同的目录中,只需运行wire。Wire将找到InitializeEvent注入器,并生成一个函数,该函数体包含所有必要的初始化步骤。 结果将被写入名为wire_gen.go的文件。
让我们看看Wire为我们做了什么:
1 2 3 4 5 6 7 | // wire_gen.go func InitializeEvent() Event { message := NewMessage() greeter := NewGreeter(message) event := NewEvent(greeter) return event } |
看起来就像我们上面写的代码一样!这仅仅是一个包含三个组件的简单示例,因此手动编写初始化程序不会太麻烦。 可以想象Wire对于复杂得多的组件将多么有用。使用Wire时,我们会将wire.go和wire_gen.go都提交给源代码管理。
使用Wire进行更改
为了展示Wire如何处理更复杂的步骤的一小部分,让我们为事件重构初始化程序以返回错误看看会发生什么
1 2 3 4 5 6 | 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 } |
我们会看到有时候一个Greeter可能很脾气暴躁(???),以至于我们不能创建一个Event。 NewGreeter初始化程序现在如下所示:
1 2 3 4 5 6 7 | func NewGreeter(m Message) Greeter { var grumpy bool if time.Now().Unix()%2 == 0 { grumpy = true } return Greeter{Message: m, Grumpy: grumpy} } |
我们已经向Greeter结构体中添加了一个“暴躁”的字段,如果initializer调用的时间是Unix时代以来的偶数秒,那么我们将创建一个暴躁的Greeter,而不是友好的Greeter。
Greet方法变成:
1 2 3 4 5 6 | func (g Greeter) Greet() Message { if g.Grumpy { return Message("Go away!") } return g.Message } |
现在知道了暴躁的Greeter对一个event有多不利了吧。因此NewEvent可能会失败。我们的main函数必须考虑到InitializeEvent可能会失败:
1 2 3 4 5 6 7 8 | func main() { e, err := InitializeEvent() if err != nil { fmt.Printf("failed to create event: %s\n", err) os.Exit(2) } e.Start() } |
我们还需要更新InitializeEvent,将错误类型添加到返回值中:
1 2 3 4 5 | // wire.go func InitializeEvent() (Event, error) { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{}, nil } |
设置完成后,我们就可以再次调用wire命令了。注意,在运行一次wire之后生成一个wire_gen。去文件,我们也可以用去生成。运行命令后,我们的wire_gen。go文件看起来是这样的:
1 2 3 4 5 6 7 8 9 10 | // 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检测到NewEvent提供程序可能会失败,并在生成的代码中执行了正确的操作:它检查错误并在出现错误时提前返回。
更改Injector的签名
另一个改进是,让我们看看Wire是如何基于injector的签名生成代码的。目前,我们已经将消息硬编码到NewMessage中。在实践中,允许调用者以合适的方式更改消息内容是更好的做法。让我们将InitializeEvent改为如下:
1 2 3 4 | func InitializeEvent(phrase string) (Event, error) { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{}, nil } |
现在InitializeEvent允许调用者传入phrase以供Greeter使用。我们还为NewMessage添加了一个phrase参数:
1 2 3 | func NewMessage(phrase string) Message { return Message(phrase) } |
在再次运行wire之后,我们将看到该工具生成了一个initializer,它将phrase值作为消息传递给Greeter。整洁!
1 2 3 4 5 6 7 8 9 10 | // 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检查injector的参数,参数列表中添加了一个字符串(例如,phrase),同样在所有提供程序中,NewMessage接受一个string,因此它将phrase传递给NewMessage。
用Errors来捕捉异常
下面来看看,当Wire检测到代码中的错误时会发生什么,以及Wire的error如何帮助我们纠正一些问题。
例如,在编写注入器InitializeEvent时,假设我们忘记为Greeter添加一个provider。看看会发生什么:
1 2 3 4 | func InitializeEvent(phrase string) (Event, error) { wire.Build(NewEvent, NewMessage) // woops! We forgot to add a provider for Greeter return Event{}, nil } |
运行wire将会看到如下提示
1 2 3 4 | # 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告诉我们一些有用的信息:无法为Greeter找到提供程序。注意,错误消息打印出了到Greeter的完整路径、发生问题的行号和注入器名称:InitializeEvent中的第24行。此外,错误消息告诉我们哪个提供者需要一个Greeter。它是事件类型。一旦我们传入一个Greeter的provider,问题就会得到解决。
或者,如果我们为wire.Build提供了太多的provider,会发生什么?
1 2 3 4 5 6 7 8 | 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贴心地告诉我们,我们有一个未使用的provider:
1 2 3 | $GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1: inject InitializeEvent: unused provider "NewEventNumber" wire: generate failed |
将未使用的provider从wire.Build调用中删除就可以解决错误。
结论
总结一下。首先,我们用相应的initializer或provider编写了一些组件。接下来,我们创建一个injector函数,指定入参和返回值。然后,我们在injector函数中填充一个调用者wire.Build,提供所有必需的provider。最后,我们运行wire命令来生成连接所有不同initializers的代码。当我们向injector添加一个参数和一个错误返回值时,再次运行 wire对生成的代码进行了所有必要的更新。
这个示例是个很小的程序,但它演示了Wire的一些强大功能,以及如何使用依赖注入来初始化代码。此外,使用Wire生成的代码看起来很像我们要编写的代码。没有生成将用户提交到Wire的定制类型(???),相反它只是生成了代码。我们可以用它做我们想做的事。最后,值得考虑的另一点是向组件initialization添加新依赖项是多么容易。只要我们告诉Wire如何提供(初始化)组件,我们就可以在依赖关系的任何地方添加该组件,Wire将处理其余的事情。
最后,值得一提的是,Wire支持这里没有讨论的许多其他特性。Provider可以在provider集中分组。它支持绑定接口、绑定值以及清除函数。有关更多信息,请参见高级特性一节。
原文链接: github.com/google/wire.
链接: google wire最佳实践.