依赖注入
要想用好Wire,首选我们需要什么是依赖注入。简单的说,依赖注入就是通过注入方式,实现代码低耦合高内聚的一种技术手段。举个例子简单说明下,已经了解的同学可以直接区下一节:
假设有Pen这样一个结构体类型(类),有DrawPoint的方法,我们需要使用它实现DrawPicture的功能。
// Point 代表点
type Point struct {
X int
Y int
}
// Pen 用于画图的笔
type Pen struct {
Color string
}
// NewPoint 工厂函数 用于实例化一直笔
func NewPen(color string) Pen {
return Pen{Color: color}
}
// DrawPoint 笔所实现的画点方法
func (pen Pen) DrawPoint(point Point) {
fmt.Printf("draw a point(%d, %d)\n", point.X, point.Y)
}
一个简单粗暴的办法:
// DrawPicture 画图
func DrawPicture() {
pen := NewPen("red") // 实例化一直红笔
pen.DrawPoint(Point{0, 0}) // 画0,0点
// 画其他点...
}
通过依赖注入方式实现
// DrawPictureByPen 最朴素依赖注入
func DrawPictureByPen(pen Pen) {
pen.DrawPoint(Point{0, 0})
// 画其他点...
}
// 配合接口注入
// PainTool 绘图工具接口
type PainTool interface {
DrawPoint(Point)
}
// DrawPictureByTool 通过参数注入接口实现依赖注入
func DrawPictureByTool(tool PainTool) {
tool.DrawPoint(Point{0, 0})
// 画其他点...
}
依赖注入方式有什么好处呢?
- 朴素方式的注入
有一说一,没有啥实在的好处。硬要说的话,就是Pen结构的变化,不会影响DrawPicture内部的代码结构。
-
配合接口进行注入的话
- 实现了依赖倒置,降低了代码的耦合度。
如果后续要使用Pencil,Brusher等进行画图,只需要实现DrawPoint方法(即实现PainTool接口),就可以直接通过参数将依赖注入。而不用修改其他地方。
- 提高代码的可测试性。
这个主要是说单元测试,如果要进行DrawPictureByPen函数,Pen的DrawPoint方法的变化可能导致测试失败(极端的情况下DrawPoint中panic),此时单元测试的对象DrawPictureByPen并不是引起测试异常的原有。而如果对DrawPictureByTool函数做单元测试,则只需要完成对PainTool接口的Mock操作,就可以确保单元测试失败,一定是DrawPictureByTool内部代码引起的。
- 代码的易读性
通过参数作为依赖注入入口,可以方便我们确认哪些是外部依赖,哪些是代码行为逻辑。一定程度上可以降低当前层级代码理解的难度。这一点理解起来不是很容易。
Wire简介
Wire是Google推出的,使用依赖注入方式自动连接组键的开源代码生成工具。在编译阶段完成注入,不使用反射、也不需要服务定位。
快速开始
举例
此处直接使用官方的例子。定义号相互依赖的几个结构体类型(类)
type Message string
type Greeter struct {
Message Message
}
type Event struct {
Greeter Greeter
}
func NewMessage() Message {
return Message("Hi there!")
}
func NewGreeter(m Message) Greeter {
return Greeter{Message: m}
}
func (g Greeter) Greet() Message {
return g.Message
}
func NewEvent(g Greeter) Event {
return Event{Greeter: g}
}
func (e Event) Start() {
msg := e.Greeter.Greet()
fmt.Println(msg)
}
手写情况下,我们要使用Event的Start方法需要使用一下代码。
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
event.Start()
而使用wire,可以直接为我们生成event的实例化代码,我们的调用可以简化为。
e := InitializeEvent()
e.Start()
使用方法
安装Wire工具
go get github.com/google/wire/cmd/wire
或
go install github.com/google/wire/cmd/wire
需要确保GOPAH/bin已经被添加到环境变量($PATH)中。
准备wire文件
在一个单独的文件中完成如下代码。注意build与package之间的空行不能省略,一些插件会自动为我们处理(甚至会加上一行//go:build wireinject),可以不用在意,继续流程即可。
//+build wireinject
package packageName
func InitializeEvent() Event {
wire.Build(NewEvent, NewGreeter, NewMessage)
return Event{}
}
执行wire命令
在wire文件所在目录下执行wire命令,命令将生成 wire_gen.go 文件,主要内容如下。
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package packageName
import (
"github.com/google/wire"
)
func InitializeEvent() Event {
message := NewMessage()
greeter := NewGreeter(message)
event := NewEvent(greeter)
return event
}
其中
go:generate wire,指明以后调用go generate命令即可执行wire命令。//+build !wireinject,是build标签,指明在非wireinject环境下忽略此文件。
高级特性
重要概念
高级特性都构建在Providers和Injectors的概念之上,者是wire的两个核心概念。
- Provider
Provider是wire中的主要机制,是用于产生实例的工厂函数。前文官方示例中的NewMessage,NewGreeter,NewEvent都是Provider。Provider是可导出的Go函数,可以通过参数指定依赖,同时也可以返回错误信息。
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
}
Providers可以通过providers set进行组织,我们通常把常用的一组providers放在一个超集(wire.Set)中。此外,超集也可以组织进入更高级的超集。
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)
事实上,当依赖关系比较复杂时,亦可以通过这个方式进行分组管理,是的依赖关系更有层次,以提高代码的可读性。如:
// FruitSet 水果的Provider超集
var FruitSet = wire.NewSet(ProvideApple, ProvideBanana, ProvidePear)
// VegetableSet 蔬菜的Provider超集
var VegetableSet = wire.NewSet(ProvideCabbage, ProvideCucumber, ProvideCarrot)
// MeatSet 肉类的Provider超集
var MeatSet = wire.NewSet(ProvidePork, ProvideBeef, ProvideChicken)
// FoodSet 食物的Porvider超集 有水果、蔬菜、肉类共同构成
var FoodSet = wire.NewSet(FruitSet, VegetableSet, MeatSet)
- Injector
我们通过Injector串联相互依赖的Provider。我们定义好Injector的函数签名(包括函数名,参数,返回值的定义),并通过wire.Build说明需要使用到的Provider,wire就会自动为我们生成按照依赖顺序执行Porvider的Injector函数体。wire.Build的参数和wire.Set要求一致,同样不需要考虑顺序。
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}
Injector的描述代码时常规的go代码,为了保证编译通过,我们可以同上方代码一样提供一个标准的返回值。wire通过读取wire.Build的参数用以生成Injector的函数提,返回值是什么并不关心。。此外,injector声明文件中其他未使用到的代码,会原封不动的生成到新文件中。
补充:
injector声明文件名并未有特殊要求。但往往习惯使用wire.go或使用带有wire标记的文件名。injector函数签名对函数名也没有明确要求,但还是推荐使用Initialize标记开头。- 首次生成是必须使用
wire名,之后的生成还可以使用go generate完成,具体情况可以根据需求而定。 - 如果觉得使用
return方式结束不方便,可以尝试使用panic
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
panic(wire.Build(foobarbaz.MegaSet))
}
接口绑定
当一个注入点为接口时,由于wire时通过类型判别来完成注入的。所以最直接的办法就是提供一个返回接口类型的provider。然而这是不符合Go代码规范的。解决方案时,使用wire.Bind对接口所需传入的示例类型进行声明。
wire.Bind的第一个参数是接口类型的值的指针,一般用new([interface])方式传入;第二个参数是所需绑定类型的实例的指针,一般用new([type])方式传入。
wire.Bind(new(Fooer), new(*MyFooer)),
每一个包含接口类型入参的Provider都需要进行接口与实例类型的绑定。
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)
结构体Providers
当我们需要对一个结构体进行注入时,可以使用wire.Struct。它的第一个参数是所需要注入的结构体类型,一般用new([type])。后续参数为所需注入的字段名称。
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 InitializeFooBar() FooBar { // 此处可以替换为*FooBar
wire.Build(Set)
return &FooBar{}
}
生成代码如下
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
补充:
- 可以使用
"*",表示所有字段都需要注入。 - 当使用
"*"使,也可以通过结构体Tag标记不需要注入的字段。
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}
- 可以通过直接修改
InitializeFooBar的返回值类型,实现对指针类型的注入。其他地方无须调整。
值绑定
有时需要为基本类型的属性绑定具体值,比如说避免nil, 这时可以使用 wire.Value进行值绑定。这可以避免掉一些无用的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会抛出错误。
- 对于接口类型的值,使用
wire.Interface进行处理
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}
结构体字段Provider
有时我们希望结构体字段作为依赖进行注入。这时我们可以使用wire.FieldOf而不用单独取写一个Get函数。
func injectedMessage() string {
wire.Build(
provideFoo,
wire.FieldsOf(new(Foo), "S"))
return ""
}
生成代码如下:
func injectedMessage() string {
foo := provideFoo()
string2 := foo.S
return string2
}
补充:
wire.FieldOf可以声明多个字段wire.FieldOf同时支持T和*T类型
清理函数
如果一个Provider创建了一个需要清理的资源,如Flie需要Close。Injector可以通过闭包的方式实现当构造出现错误时,一次清理掉所有需要清理的资源。这个清理函数需要使用func()标签。
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
}
\