最近,在负责一个 Go 项目开发。项目分层为:handler 层、service 层 dao 层。其中 handler 层依赖 service 层,service 层依赖不同基础组件以及 dao 层。这样,只要 handler 层、service 层的依赖项发生了变化,对应层的代码都得重新调整,非常繁琐。为此,在网上搜索了 Go 依赖注入相关的资料,得出,常用的 Go 依赖注入框架有:dig、inject、wire。其中 dig、inject 是在运行时注入依赖,对代码有一定的侵入性。而 wire 是代码生成工具,生成的内容也是普通的 Go 代码,只不过,在运行时不再依赖 wire。

本文主要是在自己使用 wire 后,对 wire 用户指南进行完整阅读翻译并记录。

wire 简介

Wire 是一种代码生成工具,其使用依赖注入自动连接组件。

函数参数
NewB(a A) B

wire 安装

go get github.com/google/wire/cmd/wire
确保 $GOPATH/bin 已添加到 $PATH 环境变量。

wire 基础概念

在 wire 中,有两个核心概念:Providers、Injectors。

Providers

Provider
这些函数都是常规的 go 代码。
package foobarbaz

type Foo struct {
    X int
}

// ProvideFoo 函数返回一个 Foo 结构体值。
func ProvideFoo() Foo {
    return Foo{X: 42}
}

Providers 可以通过参数指定依赖项:

package foobarbaz

type Bar struct {
    X int
}

// ProvideBar 函数通过参数指定依赖 Foo 结构体。
func ProvideBar(foo Foo) Bar {
    return Bar{X: -foo.X}
}

Providers 还可以返回 errors:

package foobarbaz

import (
    "context"
    "errors"
)

type Baz struct {
    X int
}

func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
    if bar.X == 0 {
        return Baz{}, errors.New("cannot provide baz when bar is zero")
    }
    return Baz{X: bar.X}, nil
}
ProviderSet
package foobarbaz

import (
    "github.com/google/wire"
)

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

另外,还可以将多个 ProviderSet 组合起来,形成一个新的 ProviderSet:

package foobarbaz

import (
    "example.com/some/other/pkg"
)

var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

Injectors

Injector
一个应用程序就是通过 Injector 将不同的 Providers 串联起来。

使用 Wire 编写 Injector 签名,然后运行 Wire 命令,生成函数主体。

// +build wireinject
// 此 build tag 用于确保此代码文件在最终构建阶段不会被纳入构建。

package main

import (
    "context"

    "github.com/google/wire"
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    wire.Build(foobarbaz.MegaSet)
    return foobarbaz.Baz{}, nil
}
Injectors 也支持通过参数指定依赖(参数会转交给对应的 Provider),并且也可以返回 error 类型值。

wire.Build 的参数与 wire.NewSet 作用一样:它们形成 ProviderSet。这是在生成该 Injector 代码期间使用的 ProviderSet。

在 Injector 代码文件中,找到的所有 Injector 声明,都将被复制到生成的代码文件中。

在 Injector 代码文件目录下,运行 Wire 命令:

wire
wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package main

import (
    "example.com/foobarbaz"
)

func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
    foo := foobarbaz.ProvideFoo()
    bar := foobarbaz.ProvideBar(foo)
    baz, err := foobarbaz.ProvideBaz(ctx, bar)
    if err != nil {
        return foobarbaz.Baz{}, err
    }
    return baz, nil
}

如上所示,生成的内容跟开发人员自己所写的内容很像。该内容,在运行时,对 Wire 的依赖很小(所有生成的内容都是常规的 Go 代码,无需 Wire 即可使用)。

一旦 wire_gen.go 被创建过,后续,可以直接通过运行 go generate 来重新生成。

Wire 高级特性

以下特性都是建立在 Providers 和 Injectors 概念基础之上。

绑定接口

接口类型的具体实现

Wire 是通过类型标识将输入与输出进行匹配,一般,倾向于创建一个返回接口类型的 Provider。然而,这不是惯用用法,因为 Go 最佳实践是:返回具体实现。

依赖接口,返回(接口的具体)实现。
ProviderSet接口绑定
type Fooer interface {
    Foo() string
}

type MyFooer string

// Fooer 接口的具体实现
func (b *MyFooer) Foo() string {
    return string(*b)
}

// 返回的是 Fooer 接口的具体实现
func provideMyFooer() *MyFooer {
    b := new(MyFooer)
    *b = "Hello, World!"
    return b
}

type Bar string

// 依赖 Fooer 接口
func provideBar(f Fooer) string {
    return f.Foo()
}

var Set = wire.NewSet(
    provideMyFooer,

    // 接口绑定,其实就是声明 *MyFooer 是 Fooer 接口类型。
    wire.Bind(new(Fooer), new(*MyFooer)), 

    // 这样,provideMyFooer 返回的 *MyFooer 就可以被当成 Fooer 接口类型,注入到 provideBar 中。
    provideBar,
)

wire.Bind 函数:

  • 第一个参数:所需.接口类型的值.的指针。
  • 第二个参数:接口类型的具体实现.的类型.的值.的指针。

结构体 Providers

可以使用 Providers 返回的不同类型,来构造不同的结构体。

wire.Struct

Injector 将使用每个字段类型对应的 Provider 填充每个字段。

Swire.StructS*S
type Foo int
type Bar int

func ProvideFoo() Foo {/* ... */}

func ProvideBar() Bar {/* ... */}

type FooBar struct {
    MyFoo Foo
    MyBar Bar
}

var Set = wire.NewSet(
    ProvideFoo,
    ProvideBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"))

生成代码,如下:

func injectFooBar() FooBar {
    foo := ProvideFoo()
    bar := ProvideBar()
    fooBar := FooBar{
        MyFoo: foo,
        MyBar: bar,
    }
    return fooBar
}
wire.Struct
*wire.Struct(new(FooBar),"*")
MyFoo
var Set = wire.NewSet(
    ProvideFoo,
    wire.Struct(new(FooBar), "MyFoo"))

生成代码如下:

func injectFooBar() FooBar {
    foo := ProvideFoo()
    fooBar := FooBar{
        MyFoo: foo,
    }
    return fooBar
}
*FooBar
func injectFooBar() *FooBar {
    foo := ProvideFoo()
    fooBar := &FooBar{
        MyFoo: foo,
    }
    return fooBar
}
wire:"-"*
type Foo struct {
    mu sync.Mutex `wire:"-"`
    Bar Bar
}

当使用 wire.Struct(new(Foo), "*") 构造 Foo 类型时,Wire 将忽略 mu 字段。

此外,在 wire.Struct(new(Foo), "mu") 中显式指定一个忽略的字段,也是错误的。

绑定 Values

有时,将基本值(通常为 nil)绑定到类型很有用。

不是让 Injector 依赖一次性 Provider 函数
type Foo struct {
    X int
}

func injectFoo() Foo {
    wire.Build(wire.Value(Foo{X: 42}))
    return Foo{}
}

生成代码如下:

func injectFoo() Foo {
    foo := _wireFooValue
    return foo
}

var (
    _wireFooValue = Foo{X: 42}
)
注意,该表达式被复制到 Injector 的函数中。对变量的引用将在 Injector 包装初始化时计算。如果表达式调用任何函数或从任何通道接收,则 Wire 将报错。

对于接口类型 values,使用 InterfaceValue:

func injectReader() io.Reader {
    wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
    return nil
}

将结构体字段作为 Providers

有时,用户想要的 Providers 是结构体的某些字段。

getS
type Foo struct {
    S string
    N int
    F float64
}

func getS(foo Foo) string {
    // 不好的方式!使用 wire.FieldsOf 代替。
    return foo.S
}

func provideFoo() Foo {
    return Foo{ S: "Hello, World!", N: 1, F: 3.14 }
}

func injectedMessage() string {
    wire.Build(
        provideFoo,
        getS)
    return ""
}

可以改用 wire.FieldsOf 来直接使用这些字段,而无需编写 getS 函数:

func injectedMessage() string {
    wire.Build(
        provideFoo,
        wire.FieldsOf(new(Foo), "S"))
    return ""
}

生成代码如下:

func injectedMessage() string {
    foo := provideFoo()
    string2 := foo.S
    return string2
}

按需将任意多个字段名称添加到 wire.FieldsOf 函数中。

对于给定的字段类型 T,FieldsOf 至少提供 T;如果 struct 参数是指向结构的指针,则 FieldsOf还提供 *T。

Cleanup 函数

如果 Provider 创建了一个需要清除的值(例如:关闭文件),那么,它可以返回一个闭包函数以清除资源。

如果在 Injector 的实现中稍后调用的 Provider 返回错误,则 Injector 将使用此函数将聚合的清除函数返回给调用者或清理资源。

func provideFile(log Logger, path Path) (*os.File, func(), error) {
    f, err := os.Open(string(path))
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() {
        if err := f.Close(); err != nil {
            log.Log(err)
        }
    }
    return f, cleanup, nil
}

确保在 Provider 的任何输入的清除功能之前调用清除功能,并且该清除功能必须具有签名 func()。

另一种 Provider 语法

可以使用 panic 来编写更简洁的 Provider:

func injectFoo() Foo {
    panic(wire.Build(/* ... */))
}

参考资料