01
介绍
本文介绍了 Wire 的基本使用,关于高级功能,例如提供者集合、绑定接口、绑定值、清理等,在本文中并未提及,如果您已经了解了 Wire 的基本使用,想要了解 Wire 的高级功能,本文可能并不适合您阅读。
Wire 是一种代码生成工具,可以使用依赖注入自动连接组件。组件之间的依赖关系在 Wire 中表示为函数参数,关于函数参数,鼓励显式初始化而不是全局变量。
Wire 具有两个基本概念:提供者和注入者(注入器)。
提供者是普通的 Go 函数,它们根据给定的依赖关系「提供」值,这些值被简单描述为函数的参数。我们还可以将通常一起使用的提供程序分组到 ProviderSet 中。
注入者是生成的函数,它们按依赖关系顺序调用提供程序。您写出注入者的签名(包括所有需要的输入作为参数),并插入对 wire 的调用。构建构造函数最终结果所需的提供者或提供者集合列表。
02
使用
安装 wire:
运行命令安装 wire
go get github.com/google/wire/cmd/wire
并确保将 添加到您的GOPATH/bin添加到您的GOPATH/bin 添加到您的 PATH 中。
使用 Wire
让我们通过示例学习使用 Wire。在这里,我们将构建一个小的问候程序,以了解如何使用Wire。
步骤 一:
让我们创建一个小程序,用问候者向客人发送特定消息的方式模拟事件。
首先,我们将创建三种类型:
1)给问候者的消息,
2)传达该消息的问候者,
3)以问候者打招呼开始的事件。
在此设计中,我们具有三种结构类型:
type Message string type Greeter struct { // ... TBD } type Event struct { // ... TBD }
消息类型只是包装一个字符串。现在,我们将创建一个简单的初始化程序,该初始化程序始终返回硬编码(固定字符串)的消息:
func NewMessage() Message { return Message("Hi there!") }
我们的问候者将需要参考消息。因此,我们还要为 Greeter 创建一个初始化程序。
func NewGreeter(m Message) Greeter { return Greeter{Message: m} } type Greeter struct { Message Message // <- adding a Message field }
在初始化程序中,我们将 Message 字段分配给 Greeter。现在,我们可以在Greeter 上创建 Greet 方法时使用 Message:
func (g Greeter) Greet() Message { return g.Message }
接下来,我们需要让 Event 具有一个 Greeter,因此我们还将为其创建一个初始化程序。
func NewEvent(g Greeter) Event { return Event{Greeter: g} } type Event struct { Greeter Greeter // <- adding a Greeter field }
然后我们添加一个方法来启动事件:
func (e Event) Start() { msg := e.Greeter.Greet() fmt.Println(msg) }
Start 方法是我们小型应用程序的核心:它告诉问候者发出问候语,然后将该信息打印到屏幕上。
请注意,提供者函数必须是可导出的函数,提供者函数使用参数指定依赖项。
现在我们已经准备好了应用程序的所有组件,让我们看看不使用 Wire 初始化所有组件需要做什么。我们的主要功能如下所示:
func main() { message := NewMessage() greeter := NewGreeter(message) event := NewEvent(greeter) event.Start() }
首先,我们创建一条消息,然后使用该消息创建一个问候器,最后我们使用该问候器创建一个事件。完成所有初始化后,我们就可以开始我们的事件了。
我们正在使用依赖注入设计原理。实际上,这意味着我们会传递每个组件所需的任何内容。这种设计风格使其易于编写易于测试的代码,并易于将一个依赖项与另一个依赖项交换出去。
步骤二:
使用Wire生成代码。
依赖项注入的一个缺点是需要这么多初始化步骤。让我们看看如何使用 Wire 使组件初始化过程更流畅。
接下来,在一个名为 wire.go 的单独文件中,我们将定义 InitializeEvent。这是事情变得有趣的地方:
// wire.go func InitializeEvent() Event { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{} }
我们只需要对wire进行一次调用即可,而不是麻烦地依次初始化每个组件并将其传递给下一个组件。在Wire中,构建传递要使用的初始化程序称为「提供者程序」,提供特定类型的功能。我们为 Event 添加一个零值作为返回值,以使编译器顺利运行。请注意,即使我们向 Event 添加值,Wire 也会忽略它们。实际上,注入者程序的目的是提供有关用于构造事件的提供者程序的信息,因此我们将在文件顶部使用构建约束将其从最终二进制文件中排除:
//+build wireinject
注意和 package 包声明语句隔开一个空行。
用 Wire 的话来说,InitializeEvent 是一个「注入器」。既然我们已经完成了注入器,那么就可以使用 Wire 命令行工具了。
我们在 Part 02 开头已经介绍了如何安装 Wire 命令行工具。如果您还未安装 Wire,请参考 Wire 安装部分的内容。
然后在与上面的代码相同的目录中,只需运行 wire。Wire 将找到 InitializeEvent 注入器并生成一个函数,该函数的主体包含所有必需的初始化步骤。结果将被写入名为 wire_gen.go 的文件。
让我们看看 Wire 为我们做了什么:
// wire_gen.go func InitializeEvent() Event { message := NewMessage() greeter := NewGreeter(message) event := NewEvent(greeter) return event }
看起来就像我们上面(步骤一中不使用 Wire 初始化所有组件)写的一样!现在,这是一个仅包含三个组件的简单示例,因此手动编写初始化程序不会太麻烦。想象一下,Wire 对于复杂得多的组件有多大用处。使用 Wire 时,我们会将 wire.go 和 wire_gen.go 都提交给源代码管理。
让我们开始将主函数更改为如下所示:
func main() { e := InitializeEvent() e.Start() }
请注意,您需要使用 go build 构建应用程序,然后运行生成的二进制可执行文件查看运行结果。
03
返回错误
为了展示 Wire 如何处理更复杂的设置的一小部分,让我们重构 Event 的初始化程序以返回错误并查看会发生什么。
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 }
我们会说,有时迎宾员可能会脾气暴躁,因此我们无法创建事件。NewGreeter 初始化程序现在看起来像这样:
func NewGreeter(m Message) Greeter { var grumpy bool if time.Now().Unix()%2 == 0 { grumpy = true } return Greeter{Message: m, Grumpy: grumpy} }
我们在 Greeter 结构中添加了一个 Grumpy 字段,如果自 Unix 时代以来初始化程序的调用时间是偶数秒,我们将创建一个脾气暴躁的问候者,而不是友好的问候者。
然后,Greet 方法变为:
func (g Greeter) Greet() Message { if g.Grumpy { return Message("Go away!") } return g.Message }
现在,您将看到脾气暴躁的迎宾者对事件没有好处。因此,NewEvent 可能会失败。
我们还需要更新 InitializeEvent 以将错误类型添加到返回值:
// wire.go func InitializeEvent() (Event, error) { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{}, nil }
设置完成后,我们准备再次调用 wire 命令。注意,在运行一次 wire 来生成 wire_gen.go 文件之后,我们也可以使用 go generate。运行命令后,我们的 wire_gen.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 已检测到 NewEvent 提供程序可能会失败,并已在生成的代码中完成了正确的操作:它检查错误并如果存在错误,则提早返回。
现在,我们应用程序的 main 函数必须考虑到 InitializeEvent 实际上可能会失败:
func main() { e, err := InitializeEvent() if err != nil { fmt.Printf("failed to create event: %s\n", err) os.Exit(2) } e.Start() }
04
函数签名
作为另一个改进,让我们看一下 Wire 如何根据注入器的签名生成代码。目前,我们已经在 NewMessage 中对消息进行了硬编码。在实践中,允许呼叫者更改该消息会更好。因此,让我们将 InitializeEvent 更改如下:
func InitializeEvent(phrase string) (Event, error) { wire.Build(NewEvent, NewGreeter, NewMessage) return Event{}, nil }
现在,Initialize Event 允许呼叫者传递短语以供 Greeter 使用。我们还将短语参数添加到「新消息」中:
func NewMessage(phrase string) Message { return Message(phrase) }
再次运行 wire 之后,我们将看到该工具已生成一个初始化程序,该初始化程序将短语值作为消息传递给 Greeter。
// 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 检查到注入器的参数,看到我们在参数列表中添加了一个字符串(例如,phrase),同样地,在所有提供程序中,NewMessage 接受了一个字符串,因此将短语传递给 NewMessage。
05
捕捉错误
我们还要看看 Wire 在代码中检测到错误时会发生什么,并查看 Wire 的错误消息如何帮助我们纠正问题。
例如,当编写我们的注入器 InitializeEvent 时,假设我们忘记为 Greeter 添加一个提供程序。让我们看看发生了什么:
func InitializeEvent(phrase string) (Event, error) { wire.Build(NewEvent, NewMessage) // woops! We forgot to add a provider for Greeter return Event{}, nil }
运行 wire 命令,我们看到以下内容:
# 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 的提供者,该问题就会得到解决。
另外,如果我们提供了太多的提供者来 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 告诉我们我们有一个未使用的提供程序:
$GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1: inject InitializeEvent: unused provider "NewEventNumber" wire: generate failed
从对 wire.Build 的调用中删除未使用的提供程序,即可解决该错误。
06
总结
让我们总结一下我们在这里所做的事情。首先,我们编写了许多具有相应的初始化程序或提供程序的组件。接下来,我们创建了一个注入器函数,指定了它接收的参数和返回的类型。然后,我们通过对 wire 的调用来填充注入器函数。构建提供所有必要的提供程序。最后,我们运行 wire 命令来生成将所有不同的初始化程序连接起来的代码。当我们向注入器添加一个参数和一个错误返回值时,再次运行 wire 对生成的代码进行了所有必要的更新。
这里的例子很小,但是它展示了 Wire 的一些功能,以及如何减轻使用依赖注入初始化代码的痛苦。此外,使用 Wire 生成的代码看起来很像我们原本应该编写的代码。没有将用户提交到 Wire 的定制类型。相反,它只是生成的代码。最后,值得考虑的另一点是向我们的组件初始化添加新的依赖项有多么容易。只要我们告诉 Wire 如何提供(即初始化)组件,我们就可以在依赖关系图中的任何位置添加该组件,然后 Wire 将处理其余的组件。
最后,值得一提的是,Wire 支持许多此处未讨论的其他功能。提供者可以被分组在提供者集合中。支持绑定接口,绑定值以及清理功能。