Google Wire 使用指南
一、基础
Wire 包含两个核心概念:provider 和 injector 。
1. Provider
Provider 是一个有返回值的、可导出的普通 Golang 函数:
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}
和其他函数一样,Provider 可以通过参数指定依赖:
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
Provider 也可以返回错误:
package foobarbaz
import (
"context"
"errors"
)
// ...
type Baz struct {
X int
}
// ProvideBaz returns a value if Bar is not zero.
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
}
也可以将一组相关的的 Provider 分组至 Provider set:
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
可以向已存在的 Provider set 添加 Provider:
package foobarbaz
import (
// ...
"example.com/some/other/pkg"
)
// ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)
2. Injector
Injector 是一个按依赖顺序调用 Provider 的函数,它将相关的多个 Proivder 连接起来。
在添加 //go:build wireinject
构建指令后,运行 wire 命令可以生成 Injector 函数的主体内容。
//go:build wireinject
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
}
在 Injector 的函数声明中,需要调用 wire.Build
函数,wire.Build
的参数与 wire.NewSet
一样都是 Provider 的集合。Injector 函数的返回值并不重要,在调用 wire 生成的代码中,它们会被忽略。最后会生成一个 xxx_gen.go
的文件:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go: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
}
Injector 所在文件中的任何非 injector 声明都将被复制到生成的文件中。
正如你所看到的,输出结果非常接近于开发者自己写的东西。此外,在运行时对 Wire 的依赖性很小:所有编写的代码都是正常的 Go 代码,不需要 Wire 就可以使用。
二、高级特性
1. 接口绑定
更多的情况下,依赖注入被用来为一个接口绑定一个具体的实现。
type Fooer interface {
Foo() string
}
type MyFooer string
func (b *MyFooer) Foo() string {
return string(*b)
}
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}
type Bar string
func provideBar(f Fooer) string {
// f will be a *MyFooer.
return f.Foo()
}
var Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)
此处,wire.Bind
的第一个参数是一个指向所需接口类型值的指针,第二个参数是一个指向实现该接口的类型值的指针。
任何包括接口绑定的集合必须在同一集合中也有一个提供具体类型的提供者。
2. Struct Provider
使用 wire.Struct
函数来构造一个 struct
类型,并告诉 Injector 哪些字段应该被注入。
Injector 将使用字段类型的 Provider 来填充每个字段。对于产生的结构类型 S
,wire.Struct
同时提供 S
和 *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"))
一个为 FooBar 生成的 Injector 如下:
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
wire.Struct
的第一个参数是一个指向所需结构类型的指针,随后的参数是要注入的字段的名称。一个特殊的字符串 "*"
可以作为一个快捷方式,告诉 Injector 注入所有字段。所以wire.Struct(new(FooBar), "*")
产生的结果和上述示例一样。
如果 Injector 返回一个 *FooBar
而不是一个 FooBar
,生成的 Injector 如下:
func injectFooBar() *FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := &FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
有时,防止某些字段被 Injector 填入是很有用的,特别是当把 *
传给 wire.Struct
时。
此时应给字段添加一个 wire:"-"
tag,让 Wire
忽略这些字段:
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}
如果此时使用 wire.Struct(new(Foo), "mu")
那样明确指定一个被阻止的字段将会产生一个错误。
3. 值绑定
有时候需要将一个基本值(通常是 nil)绑定到一个类型。与其让Injector 依赖一个 Provider ,不如将一个值表达式添加到一个 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}
)
如果表达式调用任何函数或从任何通道接收,Wire
将抛出一个错误。
对于 interface
的值,使用 InterfaceValue
:
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}
4. 使用结构体的字段作为 Provider
有时需要使用 struct 的某些字段作为 Provider,下面是一个错误的示例:
type Foo struct {
S string
N int
F float64
}
func getS(foo Foo) string {
// Bad! Use wire.FieldsOf instead.
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
;如果结构参数是一个指向结构的指针,那么 FieldsOf
也提供 *T
。
5. 清理函数
如果一个 Provider 创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个关闭来清理资源。Injector 将使用它来向调用者返回一个聚合的清理函数,或者在 Injector 的实现中稍后调用的 Provider 返回一个错误时清理该资源。
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()
的签名。
6. Injector 替代语法
如果你厌倦了在 Injector 函数声明的末尾写 return foobarbaz.Foo{}, nil
这种语句,可以用 panic
替代:
func injectFoo() Foo {
panic(wire.Build(/* ... */))
}
三、最佳实践
1. 类型区分
如果你需要注入一个常见的类型,如字符串,请创建一个新的字符串类型,以避免与其他 Provider 冲突:
type MySQLConnectionString string
2. Options Structs
一个包含许多依赖关系的 Provider 函数可以将该函数与 options struct
配对。
type Options struct {
// Messages is the set of recommended greetings.
Messages []Message
// Writer is the location to send greetings. nil goes to stdout.
Writer io.Writer
}
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
// ...
}
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)