原文在这里
基础知识
Wire 有两个核心概念:提供者(providers)和注入器(injectors)。
定义提供者
在 Wire 中,主要的机制是提供者:一个可以生成值的函数。这些函数是普通的 Go 代码。
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo 返回一个 Foo。
func ProvideFoo() Foo {
return Foo{X: 42}
}
提供者函数必须导出,以便从其他包中使用,就像普通函数一样。
提供者可以指定依赖项,使用参数:
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar 返回一个 Bar:一个负的 Foo。
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
提供者还可以返回错误:
package foobarbaz
import (
"context"
"errors"
)
// ...
type Baz struct {
X int
}
// ProvideBaz 返回一个值,如果 Bar 不为零。
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("当 bar 为零时无法提供 baz")
}
return Baz{X: bar.X}, nil
}
提供者可以分组成提供者集。如果几个提供者经常一起使用,这是非常有用的。要将这些提供者添加到名为 SuperSet
的新集合中,请使用 wire.NewSet
函数:
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
你还可以将其他提供者集添加到一个提供者集中。
package foobarbaz
import (
// ...
"example.com/some/other/pkg"
)
// ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)
注入器
应用程序使用注入器来连接这些提供者:注入器是一个按依赖顺序调用提供者的函数。使用 Wire,你编写注入器的签名,然后 Wire 会生成函数体。
通过编写一个函数声明并在其函数体中调用 wire.Build
来声明注入器。返回值不重要,只要它们是正确的类型。在生成的代码中,这些值本身将被忽略。假设上述提供者定义在名为 example.com/foobarbaz
的包中,以下代码将声明一个用于获取 Baz
的注入器:
// +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
}
与提供者一样,注入器可以在输入上带有参数(这些参数将发送到提供者)并返回错误。wire.Build
的参数与 wire.NewSet
相同:它们组成提供者集。这是在为该注入器生成代码时使用的提供者集。
在带有注入器的文件中找到的任何非注入器声明都将复制到生成的文件中。
你可以通过在包目录中调用 Wire 来生成注入器:
wire
Wire 将在名为 wire_gen.go
的文件中生成注入器的实现,内容类似于以下代码:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod 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 通过[类型标识][]来匹配输入和输出,因此倾向于创建一个返回接口类型的提供者函数可能并不符合 Go 的最佳实践。相反,你可以在提供者集中声明接口绑定:
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 将是 *MyFooer。
return f.Foo()
}
var Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)
wire.Bind
的第一个参数是指向所需接口类型的指针,第二个参数是指向实现接口的类型的指针。任何包含接口绑定的集合还必须在同一集合中有一个提供者,提供具体类型。
结构体提供者
可以使用提
供的类型构造结构体。使用 wire.Struct
函数来构造结构体类型,并告诉注入器应该注入哪个字段。注入器将使用字段的类型的提供者来填充每个字段。对于生成的结构体类型 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
的生成注入器将如下所示:
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
wire.Struct
的第一个参数是指向所需结构体类型的指针,后续参数是要注入的字段的名称。特殊字符串 "*"
可以用作快捷方式,告诉注入器注入所有字段。因此,wire.Struct(new(FooBar), "*")
的结果与上面的相同。
对于上面的示例,你可以通过将 Set
更改为仅注入 "MyFoo"
来指定只注入 "MyFoo"
:
var Set = wire.NewSet(
ProvideFoo,
wire.Struct(new(FooBar), "MyFoo"))
然后,FooBar
的生成注入器将如下所示:
func injectFooBar() FooBar {
foo := ProvideFoo()
fooBar := FooBar{
MyFoo: foo,
}
return fooBar
}
如果注入器返回的是 *FooBar
而不是 FooBar
,则生成的注入器将如下所示:
func injectFooBar() *FooBar {
foo := ProvideFoo()
fooBar := &FooBar{
MyFoo: foo,
}
return fooBar
}
有时候,防止注入器填充某些字段是有用的,尤其是当将 *
传递给 wire.Struct
时。你可以使用 tag
在字段上标记 wire:"-"
,让 Wire 忽略这些字段。例如:
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}
当你使用 wire.Struct(new(Foo), "*")
提供 Foo
类型时,Wire 将自动省略 mu
字段。此外,如果明确指定一个受限字段(例如 wire.Struct(new(Foo), "mu")
),将会导致错误。
绑定值
有时候,将基本值(通常是 nil
)绑定到类型是有用的。而不是让注入器依赖一个一次性的提供者函数,你可以将一个值表达式添加到提供者集中。
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 将会产生错误。
对于接口值,使用 InterfaceValue
:
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}
使用结构体的字段作为提供者
有时,用户想要的提供者是结构体的一些字段。如果你发现自己编写像下面示例中的 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
;如果结构体参数是结构体的指针,则 FieldsOf
还将提供 *T
。
清理函数
如果提供者创建的值需要清理(例如关闭文件),则可以返回一个关闭资源的闭包。注入器将使用它来要么向调用方返回一个聚合的清理函数,要么在注入器的实现中稍后调用该提供者时清理资源。
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
}
清理函数将在提供者的任何输入的清理函数之前调用,并且必须具有签名 func()
。
替代注入器语法
如果你厌倦了在注入器
函数声明的末尾编写 return foobarbaz.Foo{}, nil
,可以使用 panic
来更简洁地编写:
func injectFoo() Foo {
panic(wire.Build(/* ... */))
}
声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 恋水无意