简介

之前的一篇文章Go 每日一库之 dig介绍了 uber 开源的依赖注入框架dig。读了这篇文章后,@overtalk推荐了 Google 开源的wire工具。所以就有了今天这篇文章,感谢推荐👍

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

快速使用

先安装工具:

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

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

下面我们在一个例子中看看如何使用wire

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

  1. type Monster struct {
  2. Name string
  3. }
  4. func NewMonster() Monster {
  5. return Monster{Name: "kitty"}
  6. }

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

  1. type Player struct {
  2. Name string
  3. }
  4. func NewPlayer(name string) Player {
  5. return Player{Name: name}
  6. }

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

  1. type Mission struct {
  2. Player Player
  3. Monster Monster
  4. }
  5. func NewMission(p Player, m Monster) Mission {
  6. return Mission{p, m}
  7. }
  8. func (m Mission) Start() {
  9. fmt.Printf("%s defeats %s, world peace!\n", m.Player.Name, m.Monster.Name)
  10. }

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

  1. func main() {
  2. monster := NewMonster()
  3. player := NewPlayer("dj")
  4. mission := NewMission(player, monster)
  5. mission.Start()
  6. }

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

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

  1. //+build wireinject
  2. package main
  3. import "github.com/google/wire"
  4. func InitMission(name string) Mission {
  5. wire.Build(NewMonster, NewPlayer, NewMission)
  6. return Mission{}
  7. }

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

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

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

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

  1. // Code generated by Wire. DO NOT EDIT.
  2. //go:generate wire
  3. //+build !wireinject
  4. package main
  5. // Injectors from wire.go:
  6. func InitMission(name string) Mission {
  7. player := NewPlayer(name)
  8. monster := NewMonster()
  9. mission := NewMission(player, monster)
  10. return mission
  11. }

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

  1. func main() {
  2. mission := InitMission("dj")
  3. mission.Start()
  4. }

细心的童鞋可能发现了,wire.gowire_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。中招的默默在心里打个 1 好嘛😂

基础概念

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

参数

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

  1. func NewMonster(name string) Monster {
  2. return Monster{Name: name}
  3. }

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

  1. func InitMission(name string) Mission {
  2. player := NewPlayer(name)
  3. monster := NewMonster(name)
  4. mission := NewMission(player, monster)
  5. return mission
  6. }

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

  1. type PlayerParam string
  2. type MonsterParam string
  3. func NewPlayer(name PlayerParam) Player {
  4. return Player{Name: string(name)}
  5. }
  6. func NewMonster(name MonsterParam) Monster {
  7. return Monster{Name: string(name)}
  8. }
  9. func main() {
  10. mission := InitMission("dj", "kitty")
  11. mission.Start()
  12. }
  13. // wire.go
  14. func InitMission(p PlayerParam, m MonsterParam) Mission {
  15. wire.Build(NewPlayer, NewMonster, NewMission)
  16. return Mission{}
  17. }

生成的代码如下:

  1. func InitMission(m MonsterParam, p PlayerParam) Mission {
  2. player := NewPlayer(p)
  3. monster := NewMonster(m)
  4. mission := NewMission(player, monster)
  5. return mission
  6. }

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

错误

不是所有的构造操作都能成功,没准勇士出山前就死于小人之手:

  1. func NewPlayer(name string) (Player, error) {
  2. if time.Now().Unix()%2 == 0 {
  3. return Player{}, errors.New("player dead")
  4. }
  5. return Player{Name: name}, nil
  6. }

我们使创建随机失败,修改注入器InitMission()的签名,增加error返回值:

  1. func InitMission(name string) (Mission, error) {
  2. wire.Build(NewMonster, NewPlayer, NewMission)
  3. return Mission{}, nil
  4. }

生成的代码,会将NewPlayer()返回的错误,作为InitMission()的返回值:

  1. func InitMission(name string) (Mission, error) {
  2. player, err := NewPlayer(name)
  3. if err != nil {
  4. return Mission{}, err
  5. }
  6. monster := NewMonster()
  7. mission := NewMission(player, monster)
  8. return mission, nil
  9. }

wire遵循fail-fast的原则,错误必须被处理。如果我们的注入器不返回错误,但构造器返回错误,wire工具会报错!

高级特性

下面简单介绍一下wire的高级特性。

ProviderSet

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

  1. type EndingA struct {
  2. Player Player
  3. Monster Monster
  4. }
  5. func NewEndingA(p Player, m Monster) EndingA {
  6. return EndingA{p, m}
  7. }
  8. func (p EndingA) Appear() {
  9. fmt.Printf("%s defeats %s, world peace!\n", p.Player.Name, p.Monster.Name)
  10. }
  11. type EndingB struct {
  12. Player Player
  13. Monster Monster
  14. }
  15. func NewEndingB(p Player, m Monster) EndingB {
  16. return EndingB{p, m}
  17. }
  18. func (p EndingB) Appear() {
  19. fmt.Printf("%s defeats %s, but become monster, world darker!\n", p.Player.Name, p.Monster.Name)
  20. }

编写两个注入器:

  1. func InitEndingA(name string) EndingA {
  2. wire.Build(NewMonster, NewPlayer, NewEndingA)
  3. return EndingA{}
  4. }
  5. func InitEndingB(name string) EndingB {
  6. wire.Build(NewMonster, NewPlayer, NewEndingB)
  7. return EndingB{}
  8. }

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

  1. var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)

后续直接使用这个set

  1. func InitEndingA(name string) EndingA {
  2. wire.Build(monsterPlayerSet, NewEndingA)
  3. return EndingA{}
  4. }
  5. func InitEndingB(name string) EndingB {
  6. wire.Build(monsterPlayerSet, NewEndingB)
  7. return EndingB{}
  8. }

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

结构构造器

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

  1. var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)
  2. var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "Player", "Monster"))
  3. var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "Player", "Monster"))
  4. func InitEndingA(name string) EndingA {
  5. wire.Build(endingASet)
  6. return EndingA{}
  7. }
  8. func InitEndingB(name string) EndingB {
  9. wire.Build(endingBSet)
  10. return EndingB{}
  11. }

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

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

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

  1. func InitEndingA(name string) EndingA {
  2. player := NewPlayer(name)
  3. monster := NewMonster()
  4. endingA := EndingA{
  5. Player: player,
  6. Monster: monster,
  7. }
  8. return endingA
  9. }

绑定值

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

  1. var kitty = Monster{Name: "kitty"}
  2. func InitEndingA(name string) EndingA {
  3. wire.Build(NewPlayer, wire.Value(kitty), NewEndingA)
  4. return EndingA{}
  5. }
  6. func InitEndingB(name string) EndingB {
  7. wire.Build(NewPlayer, wire.Value(kitty), NewEndingB)
  8. return EndingB{}
  9. }

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

  1. // wire_gen.go
  2. func InitEndingA(name string) EndingA {
  3. player := NewPlayer(name)
  4. monster := _wireMonsterValue
  5. endingA := NewEndingA(player, monster)
  6. return endingA
  7. }
  8. var (
  9. _wireMonsterValue = kitty
  10. )

结构字段作为构造器

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

  1. func NewMission() Mission {
  2. p := Player{Name: "dj"}
  3. m := Monster{Name: "kitty"}
  4. return Mission{p, m}
  5. }
  6. // wire.go
  7. func InitPlayer() Player {
  8. wire.Build(NewMission, wire.FieldsOf(new(Mission), "Player"))
  9. }
  10. func InitMonster() Monster {
  11. wire.Build(NewMission, wire.FieldsOf(new(Mission), "Monster"))
  12. }
  13. // main.go
  14. func main() {
  15. p := InitPlayer()
  16. fmt.Println(p.Name)
  17. }

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

清理函数

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

  1. func NewPlayer(name string) (Player, func(), error) {
  2. cleanup := func() {
  3. fmt.Println("cleanup!")
  4. }
  5. if time.Now().Unix()%2 == 0 {
  6. return Player{}, cleanup, errors.New("player dead")
  7. }
  8. return Player{Name: name}, cleanup, nil
  9. }
  10. func main() {
  11. mission, cleanup, err := InitMission("dj")
  12. if err != nil {
  13. log.Fatal(err)
  14. }
  15. mission.Start()
  16. cleanup()
  17. }
  18. // wire.go
  19. func InitMission(name string) (Mission, func(), error) {
  20. wire.Build(NewMonster, NewPlayer, NewMission)
  21. return Mission{}, nil, nil
  22. }

一些细节

首先,我们调用wire生成wire_gen.go之后,如果wire.go文件有修改,只需要执行go generate即可。go generate很方便,我之前一篇文章写过generate,感兴趣可以看看深入理解Go之generate

总结

wire是随着go-cloud的示例[guestbook](https://github.com/google/go-cloud/tree/master/samples/guestbook)一起发布的,可以阅读guestbook看看它是怎么使用wire的。与dig不同,wire只是生成代码,不使用reflect库,性能方面是不用担心的。因为它生成的代码与你自己写的基本是一样的。如果生成的代码有性能问题,自己写大概率也会有😂。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. wire GitHub:https://github.com/google/wire
  2. Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib