原文在这里

让我们通过示例学习如何使用Wire。Wire的指南提供了工具的详细文档。对于那些渴望看到Wire应用于较大服务器的读者,Go Cloud中的guestbook示例使用Wire来初始化其组件。在这里,我们将构建一个小的问候程序,以了解如何使用Wire。完成的程序可以在与本README文件相同的目录中找到。

构建初版Greeter程序

让我们创建一个小程序,模拟一个事件,由一个问候者用特定的消息向来宾致以问候。

首先,我们创建三种类型:1)问候者的消息,2)传达该消息的问候者,以及3)以问候者向来宾致以问候开始的事件。在这个设计中,我们有三种结构类型:

type Message string

type Greeter struct {
    // ... TBD
}

type Event struct {
    // ... TBD
}

Message只是简单封装了下string。现在我们通过硬编码的方式来实现一个简单地初始化:

func NewMessage() Message {
    return Message("Hi there!")
}

Greeter需要引用Message,所以我们再创建一个Greeter的初始化:

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

type Greeter struct {
    Message Message // <- adding a Message field
}

现在我们在Greeter中新增了一个Message字段,这样就可以通过GreeterGreet方法来访问Message

func (g Greeter) Greet() Message {
    return g.Message
}

接下来使用Greeter来创建一个Event

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

type Event struct {
    Greeter Greeter // <- adding a Greeter field
}

然后新增一个Start()方法:

func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

Start()方法包含了我们小应用的核心部分:它告诉问候者发出问候并将该消息打印到屏幕上。

现在我们的应用程序所有组件都准备好了,让我们看看如何在不使用Wire的情况下初始化所有组件。我们的 main 函数将如下所示:

func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}

首先,我们创建了一个消息,然后用该消息创建一个问候者,最后再用该问候者创建一个事件。完成所有初始化后,我们准备开始我们的事件。

我们使用了依赖注入设计原则。在实践中,这意味着我们传递每个组件所需的内容。这种设计风格使得编写易于测试的代码变得容易,并且可以轻松地用另一个依赖替换它。

使用Wire生成代码

依赖注入的一个缺点是需要很多初始化步骤。让我们看看如何使用Wire使初始化我们的组件过程更加顺畅。

我们先将 main 函数改为如下所示:

func main() {
    e := InitializeEvent()

    e.Start()
}

然后,在一个名为wire.go的单独文件中,我们将定义InitializeEvent。这里将变得有趣:

// wire.go

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

与其逐个初始化每个组件并将其传递给下一个组件,我们只需调用wire.Build 并传入我们想要使用的初始化程序。在Wire中,初始化程序称为”providers”,它们提供特定类型的实例。我们添加一个Event的零值作为返回值来满足编译器。请注意,即使我们添加了一些值到Event中,Wire也会忽略它们。实际上,注入器的目的是提供关于构造Event所需的哪些providers 的信息,因此我们将在文件的顶部使用构建约束来将其排除在我们的最终二进制文件之外:

//+build wireinject

注意,构建约束需要一个空行作为结束。

在Wire的术语中,InitializeEvent 是一个”injector”。现在我们的注入器完成了,我们准备使用wire命令行工具。

使用以下命令安装该工具:

go install github.com/google/wire/cmd/wire@latest

然后在与上述代码相同的目录中,运行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.gowire_gen.go两个文件到源代码控制。

使用Wire进行更改

为了展示Wire如何处理更复杂的设置的一小部分,让我们重构Event的初始化程序,使其返回一个错误并看看会发生什么。

我们假设有时候问候者可能会有些暴躁,因此我们无法创建一个事件。现在NewEvent看起来像这样:

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可能会有些暴躁,因此我们无法创建一个事件。现在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可能会失败。我们的main函数现在必须考虑到InitializeEvent可能实际上会失败:

func main() {
    e, err := InitializeEvent()
    if err != nil {
        fmt.Printf("failed to create event: %s\n", err)
        os.Exit(2)
    }
    e.Start()
}

我们还需要更新InitializeEvent来为返回值添加一个error类型:

// 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提供程序可能会失败,并在生成的代码中做了正确的处理:它会检查错误并在有错误时提前返回。

改变注入器的签名

作为另一个改进,让我们看看Wire根据注入器的签名生成代码。目前,我们在NewMessage中硬编码了消息。实际上,最好允许调用方根据需要自行更改该消息。因此,我们将InitializeEvent修改为以下样式:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}, nil
}

现在,InitializeEvent允许调用方传入用于Greeter的短语。我们还需要将NewMessage添加一个短语参数:

func NewMessage(phrase string) Message {
    return Message(phrase)
}

运行wire之后,我们将看到该工具已经生成了一个初始化函数,并将短语值作为Message传递给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需要一个字符串参数,因此它将phrase传递给NewMessage。真是太棒了!

用有用的错误消息捕捉错误

我们还可以看看当Wire检测到我们代码中的错误时会发生什么,以及Wire的错误消息如何帮助我们纠正问题。

例如,在编写我们的注入器InitializeEvent 时,忘记为Greeter添加提供者。让我们看看会发生什么:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewMessage) // 哎呀!我们忘记为 Greeter 添加提供者
    return Event{}, nil
}

运行wire命令后,我们会看到以下内容:

$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。它是 Event 类型。一旦我们传入Greeter的提供者,问题就会解决。

或者,如果我们提供了一个太多的提供者给wire.Build 会发生什么?

func NewEventNumber() int  {
    return 1
}

func InitializeEvent(phrase string) (Event, error) {
     // 哎呀!NewEventNumber 没有使用。
    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 的调用中删除未使用的提供者即可解决该错误。

结论

让我们总结一下我们在这里所做的工作。首先,我们编写了一些带有对应初始化程序或提供者的组件。接下来,我们创建了一个注入器函数,指定它接收的参数和返回的类型。然后,我们填充了注入器函数,并通过调用wire.Build 传入了所有必要的提供者。最后,我们运行了wire 命令来生成将所有不同的初始化程序连接在一起的代码。当我们为注入器添加一个参数和一个错误返回值时,再次运行wire 命令会对我们的生成代码进行所有必要的更新。

这个示例很小,但它展示了Wire的一些功能,以及它如何让使用依赖注入初始化代码变得更加轻松。此外,使用Wire生成的代码与我们通常编写的代码非常相似。没有定制的类型将用户绑定到 Wire。相反,这只是生成的代码,我们可以根据需要进行处理。最后,值得考虑的另一个要点是,初始化组件时添加新依赖项是多么容易。只要告诉Wire如何提供(即初始化)一个组件,我们就可以在依赖图中的任何位置添加该组件,Wire 会处理其余部分。

最后,值得一提的是,Wire支持许多其他在此没有讨论的功能。提供者可以分组为提供者集。支持绑定接口绑定值,以及支持清理函数。有关更多信息,请参见高级特性部分。


孟斯特

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意