Go 每日一库之 wire

简介

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

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

快速使用

先安装工具:golang

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

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

下面咱们在一个例子中看看如何使用wire数据库

如今,咱们来到一个黑暗的世界,这个世界中有一个邪恶的怪兽。咱们用下面的结构表示,同时编写一个建立方法:segmentfault

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)
}

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

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.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参数呢?

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
}

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

错误

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

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

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

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

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

func InitMission(name string) (Mission, error) {
  player, err := NewPlayer(name)
  if err != nil {
    return Mission{}, err
  }
  monster := NewMonster()
  mission := NewMission(player, monster)
  return mission, nil
}

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

高级特性

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

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的定义处便可。

结构构造器

由于咱们的EndingAEndingB的字段只有PlayerMonster,咱们就不须要显式为它们提供构造器,能够直接使用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(结构名),后面可接任意多个参数,表示须要为该结构的哪些字段注入值。上面咱们须要注入PlayerMonster两个字段。或者咱们也可使用通配符*表示注入全部字段:

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结构,若是想得到MonsterPlayer类型的对象,就能够对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
}

一些细节

首先,咱们调用wire生成wire_gen.go以后,若是wire.go文件有修改,只须要执行go generate便可。go generate很方便,我以前一篇文章写过generate,感兴趣能够看看深刻理解Go之generate

总结

wire是随着go-cloud的示例guestbook一块儿发布的,能够阅读guestbook看看它是怎么使用wire的。与dig不一样,wire只是生成代码,不使用reflect库,性能方面是不用担忧的。由于它生成的代码与你本身写的基本是同样的。若是生成的代码有性能问题,本身写大几率也会有😂。

你们若是发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

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

个人博客

欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~

本文由博客一文多发平台 OpenWrite 发布!
相关文章
相关标签/搜索