背景

项目规模小的情况下,是否有依赖注入框架问题不大,但是当项目变大之后,有一个合适的依赖注入框架是十分必要的。通过调研,了解到的golang中常用的依赖注入框架主要有 inject dig等等。但是今天要介绍的是wire,一个编译期实现依赖注入的框架。

什么是依赖注入

依赖注入是一种标准的技术,用于通过灵活地向组件提供其工作所需的所有依赖关系来生成灵活且松耦合的代码
依赖注入是控制反转的一种实现方式,其他的实现方式还有依赖查找,他们之间的区别如下:

  1. 依赖注入是在对象创建时将对象的依赖通过某种方式注入
  2. 依赖查找是指在需要时,调用对象可以通过的框架所提供的方法获得需要的依赖

依赖注入的实现方式

依赖注入的方式主要有3种:

  1. 构造器注入
  2. setter注入
  3. 接口注入

目前业界把依赖查找的一些实现方式也当做依赖注入,例如服务定位器,所以他们之间的界限并不是很清晰

为什么选择wire

除了 wire,Go 的依赖注入框架还有 Uber 的 dig 和 Facebook 的 inject,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection),而 wire 则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection)。使用反射带来的性能损失倒是其次,更重要的是反射使得代码难以追踪和调试(反射会令Ctrl+左键失效...)。而wire 生成的代码是符合程序员常规使用习惯的代码,十分容易理解和调试。

在下面这篇 blog 中,go 团队说明了为什么不采用反射的方式实现。
https://blog.golang.org/wire

主要的原因是考虑到反射的实现会把错误带到运行时并且是难以理解和调试的,而 go 团队更愿意在编译时就发现此问题。

wire简介

wire 是 Google 开源的一个依赖注入工具。它是一个代码生成器,并不是一个框架。我们只需要在一个特殊的 go 文件中告诉wire 类型之间的依赖关系,它会自动帮我们生成代码,帮助我们创建指定类型的对象,并组装它的依赖。

基础概念

wire 有两个基础概念,Provider(构造器)和 Injector(注入器)。Provider 实际上就是创建函数,大家意会一下。我们上面InitMission就是Injector。每个注入器实际上就是一个对象的创建和初始化函数。在这个函数中,我们只需要告诉 wire 要创建什么类型的对象,这个类型的依赖,wire 工具会为我们生成一个函数完成对象的创建和初始化工作。

快速使用

先安装工具:

$ go get github.com/google/wire/cmd/wire

上面的命令会在 $GOPATH/bin 中生成一个可执行程序 wire,这就是代码生成器。我个人习惯把 $GOPATH/bin 加入系统环境变量 $PATH 中,所以可直接在命令行中执行 wire 命令。

下面我们在一个例子中看看如何使用 wire。
现在,我们来到一个黑暗的世界,这个世界中有一个邪恶的怪兽。我们用下面的结构表示,同时编写一个创建方法:

type Monster struct {
  Name string
}

func NewMonster() Monster {
  return Monster{Name: "kitty"}
}

有怪兽肯定就有勇士,结构如下,同样地它也有创建方法:

type Player struct {
  Name string
}

func NewPlayer(name string) Player {
  return Player{Name: name}
}

终于有一天,勇士完成了他的使命,战胜了怪兽:

type Mission struct {
  Player  Player
  Monster Monster
}

func NewMission(p Player, m Monster) Mission {
  return Mission{p, m}
}

func (m Mission) Start() {
  fmt.Printf("%s defeats %s, world peace!\n", m.Player.Name, m.Monster.Name)
}

这可能是某个游戏里面的场景哈,我们看如何将上面的结构组装起来放在一个应用程序中:

func main() {
  monster := NewMonster()
  player := NewPlayer("dj")
  mission := NewMission(player, monster)

  mission.Start()
}

代码量少,结构不复杂的情况下,上面的实现方式确实没什么问题。但是项目庞大到一定程度,结构之间的关系变得非常复杂的时候,这种手动创建每个依赖,然后将它们组装起来的方式就会变得异常繁琐,并且容易出错。这个时候勇士wire出现了!

wire 的要求很简单,新建一个 wire.go 文件(文件名可以随意),创建我们的初始化函数。比如,我们要创建并初始化一个Mission 对象,我们就可以这样:

//+build wireinject

package main

import "github.com/google/wire"

func InitMission(name string) Mission {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}
}

首先这个函数的返回值就是我们需要创建的对象类型,wire 只需要知道类型,return 后返回什么不重要。然后在函数中,我们调用 wire.Build() 将创建 Mission 所依赖的类型的构造器传进去。例如,需要调用 NewMission() 创建 Mission 类型,NewMission() 接受两个参数一个 Monster 类型,一个 Player 类型。Monster类型对象需要调用NewMonster() 创建,Player 类型对象需要调用 NewPlayer() 创建。所以 NewMonster()NewPlayer() 我们也需要传给 wire。

文件编写完成之后,执行wire命令:

$ wire
wire: github.com/darjun/go-daily-lib/wire/get-started/after: \
wrote D:\code\golang\src\github.com\darjun\go-daily-lib\wire\get-started\after\wire_gen.go

我们看看生成的wire_gen.go文件:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission
}

这个 InitMission() 函数是不是和我们在 main.go 中编写的代码一毛一样!接下来,我们可以直接在 main.go 调用

InitMission():
func main() {
  mission := InitMission("dj")

  mission.Start()
}

细心的童鞋可能发现了,wire.go 和 wire_gen.go 文件头部位置都有一个 +build,不过一个后面是 wireinject,另一个是 !wireinject+build 其实是 Go 语言的一个特性。类似 C/C++ 的条件编译,在执行 go build 时可传入一些选项,根据这个选项决定某些文件是否编译。wire 工具只会处理有 wireinject 的文件,所以我们的 wire.go 文件要加上这个。生成的 wire_gen.go 是给我们来使用的,wire 不需要处理,故有 !wireinject。

由于现在是两个文件,我们不能用 go run main.go 运行程序,可以用 go run . 运行。运行结果与之前的例子一模一样!

注意,如果你运行时,出现了 InitMission 重定义,那么检查一下你的 //+build wireinjectpackage main 这两行之间是否有空行,这个空行必须要有!见https://github.com/google/wire/issues/117

参数

同样细心的你应该发现了,我们上面编写的 InitMission() 函数带有一个 string 类型的参数。并且在生成的InitMission() 函数中,这个参数传给了 NewPlayer()NewPlayer() 需要 string 类型的参数,而参数类型就是string。所以生成的 InitMission() 函数中,这个参数就被传给了 NewPlayer()。如果我们让 NewMonster() 也接受一个 string 参数呢?

func NewMonster(name string) Monster {
  return Monster{Name: name}
}

那么生成的 InitMission() 函数中 NewPlayer()NewMonster() 都会得到这个参数:

func InitMission(name string) Mission {
  player := NewPlayer(name)
  monster := NewMonster(name)
  mission := NewMission(player, monster)
  return mission
}

实际上,wire 在生成代码时,构造器需要的参数(或者叫依赖)会从参数中查找或通过其它构造器生成。决定选择哪个参数或构造器完全根据类型。如果参数或构造器生成的对象有类型相同的情况,运行 wire 工具时会报错。如果我们想要定制创建行为,就需要为不同类型创建不同的参数结构:

type PlayerParam string
type MonsterParam string

func NewPlayer(name PlayerParam) Player {
  return Player{Name: string(name)}
}

func NewMonster(name MonsterParam) Monster {
  return Monster{Name: string(name)}
}

func main() {
  mission := InitMission("dj", "kitty")
  mission.Start()
}

// wire.go
func InitMission(p PlayerParam, m MonsterParam) Mission {
  wire.Build(NewPlayer, NewMonster, NewMission)
  return Mission{}
}

生成的代码如下:

func InitMission(m MonsterParam, p PlayerParam) Mission {
  player := NewPlayer(p)
  monster := NewMonster(m)
  mission := NewMission(player, monster)
  return mission
}

在参数比较复杂的时候,建议将参数放在一个结构中。

高级特性

ProviderSet

有时候可能多个类型有相同的依赖,我们每次都将相同的构造器传给 wire.Build() 不仅繁琐,而且不易维护,一个依赖修改了,所有传入 wire.Build() 的地方都要修改。为此,wire 提供了一个 ProviderSet(构造器集合),可以将多个构造器打包成一个集合,后续只需要使用这个集合即可。假设,我们有关勇士和怪兽的故事有两个结局:

type EndingA struct {
  Player  Player
  Monster Monster
}

func NewEndingA(p Player, m Monster) EndingA {
  return EndingA{p, m}
}

func (p EndingA) Appear() {
  fmt.Printf("%s defeats %s, world peace!\n", p.Player.Name, p.Monster.Name)
}

type EndingB struct {
  Player  Player
  Monster Monster
}

func NewEndingB(p Player, m Monster) EndingB {
  return EndingB{p, m}
}

func (p EndingB) Appear() {
  fmt.Printf("%s defeats %s, but become monster, world darker!\n", p.Player.Name, p.Monster.Name)
}

编写两个注入器:

func InitEndingA(name string) EndingA {
  wire.Build(NewMonster, NewPlayer, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewMonster, NewPlayer, NewEndingB)
  return EndingB{}
}

我们观察到两次调用 wire.Build() 都需要传入 NewMonsterNewPlayer。两个还好,如果很多的话写起来就麻烦了,而且修改也不容易。这种情况下,我们可以先定义一个 ProviderSet

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

后续直接使用这个set:

func InitEndingA(name string) EndingA {
  wire.Build(monsterPlayerSet, NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(monsterPlayerSet, NewEndingB)
  return EndingB{}
}

而后如果要添加或删除某个构造器,直接修改set的定义处即可。

结构构造器

因为我们的 EndingA 和 EndingB 的字段只有 Player 和 Monster,我们就不需要显式为它们提供构造器,可以直接使用 wire提供的结构构造器(Struct Provider)。结构构造器创建某个类型的结构,然后用参数或调用其它构造器填充它的字段。例如上面的例子,我们去掉 NewEndingA() 和 NewEndingB(),然后为它们提供结构构造器:

var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "Player", "Monster"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "Player", "Monster"))

func InitEndingA(name string) EndingA {
  wire.Build(endingASet)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(endingBSet)
  return EndingB{}
}

结构构造器使用 wire.Struct 注入,第一个参数固定为 new(结构名),后面可接任意多个参数,表示需要为该结构的哪些字段注入值。上面我们需要注入 Player 和 Monster 两个字段。或者我们也可以使用通配符*表示注入所有字段:

var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "*"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "*"))

wire为我们生成正确的代码,非常棒:

func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := NewMonster()
  endingA := EndingA{
    Player:  player,
    Monster: monster,
  }
  return endingA
}

绑定值

有时候,我们需要为某个类型绑定一个值,而不想依赖构造器每次都创建一个新的值。有些类型天生就是单例,例如配置,数据库对象(sql.DB)。这时我们可以使用 wire.Value 绑定值,使用 wire.InterfaceValue 绑定接口。例如,我们的怪兽一直是一个 Kitty,我们就不用每次都去创建它了,直接绑定这个值就 ok 了:

var kitty = Monster{Name: "kitty"}

func InitEndingA(name string) EndingA {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingA)
  return EndingA{}
}

func InitEndingB(name string) EndingB {
  wire.Build(NewPlayer, wire.Value(kitty), NewEndingB)
  return EndingB{}
}

注意一点,这个值每次使用时都会拷贝,需要确保拷贝无副作用:

// wire_gen.go
func InitEndingA(name string) EndingA {
  player := NewPlayer(name)
  monster := _wireMonsterValue
  endingA := NewEndingA(player, monster)
  return endingA
}

var (
  _wireMonsterValue = kitty
)

结构字段作为构造器

有时候我们编写一个构造器,只是简单的返回某个结构的一个字段,这时可以使用 wire.FieldsOf 简化操作。现在我们直接创建了 Mission 结构,如果想获得 Monster 和 Player 类型的对象,就可以对 Mission 使用 wire.FieldsOf:

func NewMission() Mission {
  p := Player{Name: "dj"}
  m := Monster{Name: "kitty"}

  return Mission{p, m}
}

// wire.go
func InitPlayer() Player {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Player"))
}

func InitMonster() Monster {
  wire.Build(NewMission, wire.FieldsOf(new(Mission), "Monster"))
}

// main.go
func main() {
  p := InitPlayer()
  fmt.Println(p.Name)
}

同样的,第一个参数为new(结构名),后面跟多个参数表示将哪些字段作为构造器,*表示全部。

清理函数

构造器可以提供一个清理函数,如果后续的构造器返回失败,前面构造器返回的清理函数都会调用:

func NewPlayer(name string) (Player, func(), error) {
  cleanup := func() {
    fmt.Println("cleanup!")
  }
  if time.Now().Unix()%2 == 0 {
    return Player{}, cleanup, errors.New("player dead")
  }
  return Player{Name: name}, cleanup, nil
}

func main() {
  mission, cleanup, err := InitMission("dj")
  if err != nil {
    log.Fatal(err)
  }

  mission.Start()
  cleanup()
}

// wire.go
func InitMission(name string) (Mission, func(), error) {
  wire.Build(NewMonster, NewPlayer, NewMission)
  return Mission{}, nil, nil
}

使用场景

场景一:成员变量是结构体类型

用法一: 直接通过各 struct 的 new 函数拼装

自定义了所有 struct 的 new 函数, 通过 wire.Build() 组装
wire.Build.png

func InitService1() *Service {
    wire.Build(NewService1, NewRemoteCallA, NewRemoteCallB)
    return &Service{}
}

用法二: 使用ProviderSet(可包含大于等于1个new函数)

将 New 函数放到 ProviderSet 里, 再传到 wire.Build 函数内
wire.Build.png

var RemoteCallSet = wire.NewSet(NewRemoteCallA, NewRemoteCallB)

func InitService2() *Service {
    wire.Build(NewService1, RemoteCallSet)
    return &Service{}
}

ProviderSet 里可以是其他 ProviderSet 千层饼一样嵌套下去

用法三: 最上层struct不需要提供new函数, 以wire.Struct代替

使用 wire.Struct() 代替最终要生产的struct的New函数
第二个参数 "*" 表示所有字段都进行初始化
wire.Value() 作用是将值转化为一个 ProviderSet, 以满足 wire.Build() 入参类型
wire.Build.png

func InitService3() *Service {
    wire.Build(wire.Struct(new(Service), "*"), NewRemoteCallA, NewRemoteCallB, wire.Value("a"))
    return &Service{}
}

用法四: wire.Struct 指定具体字段

wire.Struct() 不使用 "*" 而是指定具体字段名
wire.Build.png

func InitService4() *Service {
    wire.Build(wire.Struct(new(Service), "remoteCallA"), NewRemoteCallA)
    return &Service{}
}

用法五:部分字段由入参传入

部分字段通过入参传入, 而不是通过 wire.Build 中指定
wire.Build.png

func InitService5(rb *RemoteCallB, c string) *Service {
    wire.Build(wire.Struct(new(Service), "*"), NewRemoteCallA)
    return &Service{}
}

需要注意的是结构体里不能有相同类型的两个及以上的字段, 如:

type Service struct {
  a string
  b string
  // ... 其他字段
}

是不允许的, 解决方法就是使用类型别名解决冲突, 会报错

provider struct has multiple fields of type string

场景二: 成员变量为interface类型

用法一: 使用返回 interface 的 new 函数

wire.Build.png

func InitService7() *Service2 {
    wire.Build(wire.Struct(new(Service2), "*"), NewRemoteCallInterface, wire.Value("a"))
    return &Service2{}
}

用法二: 使用ProviderSet, 将具体struct的New函数绑定到interface上

wire.Build.png

var RemoteCallASet = wire.NewSet(NewRemoteCallA, wire.Bind(new(RemoteCall), new(*RemoteCallA)))

func InitService8() *Service2 {
    wire.Build(wire.Struct(new(Service2), "*"), RemoteCallASet, wire.Value("a"))
    return &Service2{}
}


参考

wire GitHub:https://github.com/google/wire
https://blog.golang.org/wire

最后修改:2020 年 12 月 28 日 03 : 38 PM
如果觉得我的文章对你有用,请随意赞赏