以前的一篇文章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.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 wireinject
與package 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
的高級特性。
有時候可能多個類型有相同的依賴,咱們每次都將相同的構造器傳給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()
都須要傳入NewMonster
和NewPlayer
。兩個還好,若是不少的話寫起來就麻煩了,並且修改也不容易。這種狀況下,咱們能夠先定義一個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 }
首先,咱們調用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😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~
本文由博客一文多發平臺 OpenWrite 發佈!