以前的一篇文章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
。數據庫
如今,咱們來到一個黑暗的世界,這個世界中有一個邪惡的怪獸。咱們用下面的結構表示,同時編寫一個建立方法:微信
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}
}
複製代碼
終於有一天,勇士完成了他的使命,打敗了怪獸:ide
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 wireinject
與package main
這兩行之間是否有空行,這個空行必需要有!見github.com/google/wire…。中招的默默在內心打個 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 發佈!