Scott

wire教程 3 months ago

8313个字符
共有26人围观

[文件内容开始]

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.gowire_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 可能会心情不好,因此我们无法创建 EventNewGreeter 初始化函数现在如下所示:

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 }

现在你可以看到,心情不好的 GreeterEvent 来说是不合适的。因此,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 支持许多此处未讨论的其他功能。提供者可以分组到提供者集中。还支持绑定接口绑定值以及清理函数。有关更多信息,请参阅高级功能部分。

[文件内容结束]