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最佳实践.