0%

Go每周一库之wire

wire 是go语言的自动初始化代码生成工具,可以根据对象的依赖关系生成初始化对象并自动连接组件,通过自动生成代码的方式在编译期完成依赖注入;其实它的代码生成功能和手写无异,在简化工作上并没有太大的感受,但是我更多的是想推荐它的编程思想,将模块进行解耦,更容易维护;

安装

1
go install github.com/google/wire/cmd/wire@latest

两个核心概念 provider 和 injector

provider提供者

可以理解为对象的提供者;其实就是一个函数,它可能依赖一个对象,并且返回一个对象;以下三个函数都可以称为提供者;

1
2
3
4
5
6
7
type Foo struct {
X int
}

func ProvideFoo() Foo {
return Foo{X: 24}
}
1
2
3
4
5
6
7
type Bar struct {
X int
}

func ProvideBar(foo Foo) Bar {
return Bar{X: foo.X}
}
1
2
3
4
5
6
7
type Baz struct {
X int
}

func ProvideBaz(ctx context.Context, bar Bar) Baz {
return Baz{X: bar.X}
}

提供者可以使用wire.NewSet()合并成提供者集;

1
var Set = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

injector注入器

可以使用提供者集的依赖关系通过wire生成初始化代码;

1
2
3
4
func initialize(ctx context.Context) Baz {
wire.Build(Set)
return Baz{}
}

前面我们已经安装了wire,通过wire命令执行生成代码,生成wire_gen.go文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Code generated by Wire. DO NOT EDIT.

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

package main

import (
"context"
)

// Injectors from wire.go:

func initialize(ctx context.Context) Baz {
foo := ProvideFoo()
bar := ProvideBar(foo)
baz := ProvideBaz(ctx, bar)
return baz
}

现在有两个initialize函数,但是上面那个其实是代码生成用的,所以我们可以在文件顶部添加如下来忽略:

1
// +build wireinject

高级功能

绑定接口

当提供者注入的对象是接口且另一个提供者返回的实现接口的对象,这时就需要通过wind.Bind()绑定接口来确认依赖关系;示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Fooer interface {
Foo() int
}
type MyFoo int

func (f *MyFoo) Foo() int {
return int(*f)
}

func ProvideMyFoo() *MyFoo {
f := MyFoo(1)
return &f
}

func ProvideMyBaz(f Fooer) Baz {
return Baz{f.Foo()}
}

其中ProvideMyBaz()依赖Fooer,而ProvideMyFoo返回的是MyFoo,MyFoo实现了Fooer接口;可以通过下面的方式确认依赖关系;

1
var BindSet = wire.NewSet(ProvideMyFoo, ProvideMyBaz, wire.Bind(new(Fooer), new(*MyFoo)))

结构结构者

当需要将提供者注入到结构体中的某个字段中时,可以通过wire.Struct()来进行依赖注入,先看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type AType int
type BType int

type SType struct {
A *AType
B BType
}

func ProvideA() *AType {
i := AType(1)
return &i
}

func ProvideB() BType {
return 2
}

我们想将ATypeBType注入到SType,更传统的方法可能是直接再写一个提供者,如下:

1
2
3
4
5
6
7
8
func ProvideS(a *AType, b BType) SType {
return SType{
A: a,
B: b,
}
}

var StructSet = wire.NewSet(ProvideB, ProvideA, ProvideS)

但是我们也可以使用wire.Struct()实现结构提供者更方便的依赖注入,需要指明要注入的字段名称;也可以用*表示要注入所有字段;在注入使用字段时,如果有不需要注入的字段可以通过wire:"-"标签来进行忽略;

1
2
3
var StructSet = wire.NewSet(ProvideB, ProvideA, wire.Struct(new(SType), "A", "B"))
// 或
var StructSet = wire.NewSet(ProvideB, ProvideA, wire.Struct(new(SType), "*"))

绑定值

可以通过wire.Value()将默认值注入到结构体当中,接口类型使用wire.InterfaceValue();

1
2
3
4
5
6
7
8
9
10
wire.Build(wire.Value(Value{
A: 1,
B: 2,
}))

//...
type Value struct {
A int
B int
}

生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
func initialize(ctx context.Context) Value {
value := _wireValueValue
return value
}

var (
_wireValueValue = Value{
A: 1,
B: 2,
}
)

使用结构字段作为提供者

可以通过wire.FieldsOf来使用结构体字段来作为提供者;通常如果不使用这个方法我们就需要写一个函数来获取结构体中的某个字段;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Value struct {
A int
B int
}

func ProvideValue() Value {
return Value{
A: 1,
B: 2,
}
}

func GetA(v Value) int {
return v.A
}

var ValueSet = wire.NewSet(ProvideValue, GetA)

其中GetA其实就是获取结构体Value中的A字段,如下通过使用wire.FieldsOf可以大大简化代码:

1
var ValueSet = wire.NewSet(ProvideValue, wire.FieldsOf(new(Value), "A"))

回调功能

如果提供者需要一个回调方法(如关闭文件),那么它可以返回一个闭包来进行回调。注入器将使用它向调用者返回一个聚合的清理函数,或者如果稍后在注入器的实现中调用的提供者返回错误或使用完毕,则进行回调清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.Error())
}
log.Log("close success")
}
return f, cleanup, nil

}

完整提供者如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Logger interface {
Log(s string)
}

type Log struct {
}

func (l *Log) Log(s string) {
fmt.Println("log:" + s)
}

func provideLog() *Log {
return &Log{}
}

type Path string

func providePath() Path {
return "a.txt"
}

var FileSet = wire.NewSet(provideFile, provideLog, providePath, wire.Bind(new(Logger), new(*Log)))

注入器如下, 可以通过panic省去return默认值:

1
2
3
func initialize(ctx context.Context) (*os.File, func(), error) {
panic(wire.Build(FileSet))
}

wire生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
...

func initialize(ctx context.Context) (*os.File, func(), error) {
log := provideLog()
path := providePath()
file, cleanup, err := provideFile(log, path)
if err != nil {
return nil, nil, err
}
return file, func() {
cleanup()
}, nil
}

最后测试运行一下, 运行结果打印文件a.txt的内容和close success

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
file, cleanup, err := initialize(context.TODO())
defer cleanup()
if err != nil {
fmt.Println(err)
return
}
buf := make([]byte, 1024)
data := make([]byte, 0)
for {
i, err := file.Read(buf)
if err != nil && err != io.EOF {
return
}
if i == 0 {
break
}
data = append(data, buf[:i]...)
}
fmt.Println(string(data))
}

最后

看完还不明白,建议查看github上的官方文档,上面详细介绍wire的用法和实践;