wire教程 3 months ago
[文件内容开始]
Wire 教程
让我们通过示例学习如何使用 Wire。Wire 指南提供了该工具使用的详细文档。对于希望看到 Wire 在大型服务器中应用的读者,Go Cloud 中的留言板示例使用 Wire 初始化其组件。在这里,我们将构建一个简单的问候程序来理解 Wire 的用法。完成后的代码可以在与本 README 相同的目录中找到。
初次构建问候程序
让我们创建一个模拟活动的小程序,其中一位问候者用特定消息向客人打招呼。
首先,我们将创建三种类型:1) 问候者的消息,2) 传递该消息的问候者,3) 以问候者向客人打招呼开始的活动。在这个设计中,我们有以下三个 struct
类型:
”`go
type Message string
type Greeter struct {
// … 待定
}
type Event struct {
// … 待定
}
”`
Message
类型仅包装了一个字符串。目前,我们将创建一个简单的初始化函数,始终返回一个硬编码的消息:
go
func NewMessage() Message {
return Message("你好!")
}
我们的 Greeter
需要引用 Message
。因此,我们还需要为 Greeter
创建一个初始化函数。
”`go
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
type Greeter struct {
Message Message // <- 添加 Message 字段
}
”`
在初始化函数中,我们将 Message
字段赋值给 Greeter
。现在,我们可以在 Greeter
上创建一个 Greet
方法来使用 Message
:
go
func (g Greeter) Greet() Message {
return g.Message
}
接下来,我们的 Event
需要一个 Greeter
,因此我们也将为其创建一个初始化函数。
”`go
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
type Event struct {
Greeter Greeter // <- 添加 Greeter 字段
}
”`
然后,我们为 Event
添加一个启动方法:
go
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
Start
方法是我们小程序的核心:它让问候者发出问候,然后将该消息打印到屏幕上。
现在我们已经准备好了应用程序的所有组件,让我们看看在不使用 Wire 的情况下初始化所有组件需要做什么。我们的 main
函数将如下所示:
”`go
func main() {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
}
”`
首先我们创建一个消息,然后用该消息创建一个问候者,最后用该问候者创建一个活动。初始化完成后,我们就可以启动活动了。
我们使用了依赖注入的设计原则。在实践中,这意味着我们传入每个组件所需的内容。这种设计风格使得代码易于测试,并且可以轻松地用一个依赖项替换另一个依赖项。
使用 Wire 生成代码
依赖注入的一个缺点是初始化步骤太多。让我们看看如何使用 Wire 使初始化组件的过程更加顺畅。
首先,将我们的 main
函数改为如下形式:
”`go
func main() {
e := InitializeEvent()
e.Start()
}
”`
接下来,在一个名为 wire.go
的单独文件中,我们将定义 InitializeEvent
。这是有趣的地方:
”`go
// wire.go
func InitializeEvent() Event {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}
}
”`
我们不再需要依次初始化每个组件并将其传递给下一个组件,而是通过调用 wire.Build
传入我们想要使用的初始化函数。在 Wire 中,初始化函数被称为“提供者”,即提供特定类型的函数。我们为 Event
添加了一个零值作为返回值以满足编译器。请注意,即使我们为 Event
添加值,Wire 也会忽略它们。实际上,注入器的目的是提供有关使用哪些提供者来构造 Event
的信息,因此我们将在文件顶部使用构建约束将其从最终二进制文件中排除:
”`go
//+build wireinject
”`
注意,构建约束需要一个空白的尾随行。
在 Wire 的术语中,InitializeEvent
是一个“注入器”。现在我们的注入器已经完成,我们可以使用 wire
命令行工具了。
通过以下命令安装该工具:
shell
go install github.com/google/wire/cmd/wire@latest
然后在包含上述代码的目录中运行 wire
。Wire 将找到 InitializeEvent
注入器,并生成一个函数体,其中填充了所有必要的初始化步骤。结果将写入名为 wire_gen.go
的文件中。
让我们看看 Wire 为我们做了什么:
”`go
// 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 如何处理更复杂的设置,让我们重构 Event
的初始化函数以返回一个错误,看看会发生什么。
go
func NewEvent(g Greeter) (Event, error) {
if g.Grumpy {
return Event{}, errors.New("无法创建活动:问候者心情不好")
}
return Event{Greeter: g}, nil
}
我们假设有时 Greeter
可能会心情不好,因此我们无法创建 Event
。NewGreeter
初始化函数现在如下所示:
go
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
方法变为:
go
func (g Greeter) Greet() Message {
if g.Grumpy {
return Message("走开!")
}
return g.Message
}
现在你可以看到,心情不好的 Greeter
对 Event
来说是不合适的。因此,NewEvent
可能会失败。我们的 main
现在必须考虑到 InitializeEvent
可能会失败:
go
func main() {
e, err := InitializeEvent()
if err != nil {
fmt.Printf("创建活动失败: %s\n", err)
os.Exit(2)
}
e.Start()
}
我们还需要更新 InitializeEvent
,为返回值添加 error
类型:
”`go
// wire.go
func InitializeEvent() (Event, error) {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}, nil
}
”`
设置完成后,我们准备再次调用 wire
命令。注意,在运行 wire
生成 wire_gen.go
文件后,我们也可以使用 go generate
。运行命令后,我们的 wire_gen.go
文件如下所示:
”`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
提供者可能会失败,并在生成的代码中做了正确的事情:检查错误并在出现错误时提前返回。
更改注入器签名
作为另一个改进,让我们看看 Wire 如何根据注入器的签名生成代码。目前,我们在 NewMessage
中硬编码了消息。在实践中,允许调用者根据需要更改消息会更加方便。因此,我们将 InitializeEvent
改为如下形式:
go
func InitializeEvent(phrase string) (Event, error) {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}, nil
}
现在 InitializeEvent
允许调用者传入 Greeter
使用的 phrase
。我们还将 phrase
参数添加到 NewMessage
中:
go
func NewMessage(phrase string) Message {
return Message(phrase)
}
再次运行 wire
后,我们会看到该工具生成了一个初始化函数,将 phrase
值作为 Message
传递给 Greeter
。很棒!
”`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 检查注入器的参数,发现我们添加了一个字符串(例如 phrase
),并且在所有提供者中,NewMessage
接受一个字符串,因此它将 phrase
传递给 NewMessage
。
通过有用的错误捕捉错误
让我们看看 Wire 在检测到代码错误时会发生什么,以及 Wire 的错误消息如何帮助我们纠正问题。
例如,在编写注入器 InitializeEvent
时,假设我们忘记为 Greeter
添加提供者。让我们看看会发生什么:
go
func InitializeEvent(phrase string) (Event, error) {
wire.Build(NewEvent, NewMessage) // 哎呀!我们忘记为 Greeter 添加提供者
return Event{}, nil
}
运行 wire
后,我们会看到以下内容:
”`shell
为了可读性,将错误信息换行
$GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1:
注入 InitializeEvent: 未找到 github.com/google/wire/_tutorial.Greeter 的提供者
(由 github.com/google/wire/_tutorial.Event 的提供者需要)
wire: 生成失败
”`
Wire 告诉我们一些有用的信息:它找不到 Greeter
的提供者。注意,错误消息打印出了 Greeter
类型的完整路径。它还告诉我们问题发生的行号和注入器名称:第 24 行的 InitializeEvent
。此外,错误消息告诉我们哪个提供者需要 Greeter
。是 Event
类型。一旦我们传入 Greeter
的提供者,问题就会解决。
或者,如果我们向 wire.Build
提供了过多的提供者会发生什么?
”`go
func NewEventNumber() int {
return 1
}
func InitializeEvent(phrase string) (Event, error) {
// 哎呀!NewEventNumber 未被使用。
wire.Build(NewEvent, NewGreeter, NewMessage, NewEventNumber)
return Event{}, nil
}
”`
Wire 会告诉我们有一个未使用的提供者:
shell
$GOPATH/src/github.com/google/wire/_tutorial/wire.go:24:1:
注入 InitializeEvent: 未使用的提供者 "NewEventNumber"
wire: 生成失败
从 wire.Build
调用中删除未使用的提供者可以解决错误。
结论
让我们总结一下我们在这里做了什么。首先,我们编写了一些组件及其对应的初始化函数(即提供者)。接下来,我们创建了一个注入器函数,指定它接收的参数和返回的类型。然后,我们在注入器函数中填充了对 wire.Build
的调用,提供了所有必要的提供者。最后,我们运行 wire
命令生成连接所有不同初始化函数的代码。当我们向注入器添加参数和错误返回值时,再次运行 wire
会对生成的代码进行所有必要的更新。
这里的示例很小,但它展示了 Wire 的部分功能,以及它如何消除使用依赖注入初始化代码的许多麻烦。此外,使用 Wire 生成的代码与我们手动编写的代码非常相似。没有特定于 Wire 的自定义类型。它只是生成的代码。我们可以随意使用它。最后,另一个值得考虑的点是向组件初始化添加新依赖项有多么容易。只要我们告诉 Wire 如何提供(即初始化)一个组件,我们就可以将该组件添加到依赖图中的任何位置,Wire 将处理其余的事情。
最后,值得一提的是,Wire 支持许多此处未讨论的其他功能。提供者可以分组到提供者集中。还支持绑定接口、绑定值以及清理函数。有关更多信息,请参阅高级功能部分。
[文件内容结束]