基础知识
Wire 有两个核心概念:提供者和注入器。
定义提供者
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
}
// 如果 Bar 不为零,则 ProvideBaz 返回一个值。
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 will be a *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"
来指定:
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
时。您可以使用 `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 {
// 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
。
清理函数
如果提供程序创建需要清理的值(例如关闭文件),则它可以返回一个闭包以清理资源。注入器将使用此函数返回聚合清理函数,以将其返回给调用方或在注入器实现后面调用的提供程序返回错误时清理资源。
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(/* ... */))
}
评论(0)