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 發佈!
相關文章
相關標籤/搜索