Go 每日一庫之 wire

熱烈歡迎你,相識是一種緣分,Echa 哥為了你的到來特意準備了一份驚喜,go學習資料《「 》


Go 每日一庫之 wire


簡介

之前的一篇文章《 》介紹了 uber 開源的依賴注入框架dig。讀了這篇文章後,@overtalk推薦了 Google 開源的wire工具。所以就有了今天這篇文章,感謝推薦

wire是 Google 開源的一個依賴注入工具。它是一個代碼生成器,並不是一個框架。我們只需要在一個特殊的go文件中告訴wire類型之間的依賴關係,它會自動幫我們生成代碼,幫助我們創建指定類型的對象,並組裝它的依賴。

快速使用

先安裝工具:

<code>$ go get github.com/google/wire/cmd/wire
複製代碼/<code>

上面的命令會在$GOPATH/bin中生成一個可執行程序wire,這就是代碼生成器。我個人習慣把$GOPATH/bin加入系統環境變量$PATH中,所以可直接在命令行中執行wire命令。

下面我們在一個例子中看看如何使用wire。

現在,我們來到一個黑暗的世界,這個世界中有一個邪惡的怪獸。我們用下面的結構表示,同時編寫一個創建方法:

<code>type Monster struct {
Name string
}

func NewMonster() Monster {
return Monster{Name: "kitty"}
}
複製代碼/<code>

有怪獸肯定就有勇士,結構如下,同樣地它也有創建方法:

<code>type Player struct {
Name string
}

func NewPlayer(name string) Player {
return Player{Name: name}
}
複製代碼/<code>

終於有一天,勇士完成了他的使命,戰勝了怪獸:

<code>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)
}
複製代碼/<code>

這可能是某個遊戲裡面的場景哈,我們看如何將上面的結構組裝起來放在一個應用程序中:

<code>func main() { 

monster := NewMonster()
player := NewPlayer("dj")
mission := NewMission(player, monster)

mission.Start()
}
複製代碼/<code>

代碼量少,結構不復雜的情況下,上面的實現方式確實沒什麼問題。但是項目龐大到一定程度,結構之間的關係變得非常複雜的時候,這種手動創建每個依賴,然後將它們組裝起來的方式就會變得異常繁瑣,並且容易出錯。這個時候勇士wire出現了!

wire的要求很簡單,新建一個wire.go文件(文件名可以隨意),創建我們的初始化函數。比如,我們要創建並初始化一個Mission對象,我們就可以這樣:

<code>//+build wireinject

package main

import "github.com/google/wire"

func InitMission(name string) Mission {
wire.Build(NewMonster, NewPlayer, NewMission)
return Mission{}
}
複製代碼/<code>

首先這個函數的返回值就是我們需要創建的對象類型,wire只需要知道類型,return後返回什麼不重要。然後在函數中,我們調用wire.Build()將創建Mission所依賴的類型的構造器傳進去。例如,需要調用NewMission()創建Mission類型,NewMission()接受兩個參數一個Monster類型,一個Player類型。Monster類型對象需要調用NewMonster()創建,Player類型對象需要調用NewPlayer()創建。所以NewMonster()和NewPlayer()我們也需要傳給wire。

文件編寫完成之後,執行wire命令:

<code>$ 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
複製代碼/<code>

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

<code>// 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
}
複製代碼/<code>

這個InitMission()函數是不是和我們在main.go中編寫的代碼一毛一樣!接下來,我們可以直接在main.go調用InitMission():

<code>func main() {
mission := InitMission("dj")

mission.Start()
}
複製代碼/<code>

細心的童鞋可能發現了,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參數呢?

<code>func NewMonster(name string) Monster {
return Monster{Name: name}
}
複製代碼/<code>

那麼生成的InitMission()函數中NewPlayer()和NewMonster()都會得到這個參數:

<code>func InitMission(name string) Mission {
player := NewPlayer(name)
monster := NewMonster(name)
mission := NewMission(player, monster)
return mission
}
複製代碼/<code>

實際上,wire在生成代碼時,構造器需要的參數(或者叫依賴)會從參數中查找或通過其它構造器生成。決定選擇哪個參數或構造器完全根據類型。如果參數或構造器生成的對象有類型相同的情況,運行wire工具時會報錯。如果我們想要定製創建行為,就需要為不同類型創建不同的參數結構:

<code>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{}
}
複製代碼/<code>

生成的代碼如下:

<code>func InitMission(m MonsterParam, p PlayerParam) Mission {
player := NewPlayer(p)
monster := NewMonster(m)
mission := NewMission(player, monster)
return mission
}
複製代碼/<code>

在參數比較複雜的時候,建議將參數放在一個結構中。

錯誤

不是所有的構造操作都能成功,沒準勇士出山前就死於小人之手:

<code>func NewPlayer(name string) (Player, error) {
if time.Now().Unix()%2 == 0 {
return Player{}, errors.New("player dead")
}
return Player{Name: name}, nil
}
複製代碼/<code>

我們使創建隨機失敗,修改注入器InitMission()的簽名,增加error返回值:

<code>func InitMission(name string) (Mission, error) {
wire.Build(NewMonster, NewPlayer, NewMission)
return Mission{}, nil
}
複製代碼/<code>

生成的代碼,會將NewPlayer()返回的錯誤,作為InitMission()的返回值:

<code>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
}
複製代碼/<code>

wire遵循fail-fast的原則,錯誤必須被處理。如果我們的注入器不返回錯誤,但構造器返回錯誤,wire工具會報錯!

高級特性

下面簡單介紹一下wire的高級特性。

ProviderSet

有時候可能多個類型有相同的依賴,我們每次都將相同的構造器傳給wire.Build()不僅繁瑣,而且不易維護,一個依賴修改了,所有傳入wire.Build()的地方都要修改。為此,wire提供了一個ProviderSet(構造器集合),可以將多個構造器打包成一個集合,後續只需要使用這個集合即可。假設,我們有關勇士和怪獸的故事有兩個結局:

<code>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)
}
複製代碼/<code>

編寫兩個注入器:

<code>func InitEndingA(name string) EndingA {
wire.Build(NewMonster, NewPlayer, NewEndingA)
return EndingA{}
}

func InitEndingB(name string) EndingB {
wire.Build(NewMonster, NewPlayer, NewEndingB)
return EndingB{}
}
複製代碼/<code>

我們觀察到兩次調用wire.Build()都需要傳入NewMonster和NewPlayer。兩個還好,如果很多的話寫起來就麻煩了,而且修改也不容易。這種情況下,我們可以先定義一個ProviderSet:

<code>var monsterPlayerSet = wire.NewSet(NewMonster, NewPlayer)
複製代碼/<code>

後續直接使用這個set:

<code>func InitEndingA(name string) EndingA {
wire.Build(monsterPlayerSet, NewEndingA)
return EndingA{}
}


func InitEndingB(name string) EndingB {
wire.Build(monsterPlayerSet, NewEndingB)
return EndingB{}
}
複製代碼/<code>

而後如果要添加或刪除某個構造器,直接修改set的定義處即可。

結構構造器

因為我們的EndingA和EndingB的字段只有Player和Monster,我們就不需要顯式為它們提供構造器,可以直接使用wire提供的結構構造器(Struct Provider)。結構構造器創建某個類型的結構,然後用參數或調用其它構造器填充它的字段。例如上面的例子,我們去掉NewEndingA()和NewEndingB(),然後為它們提供結構構造器:

<code>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{}
}
複製代碼/<code>

結構構造器使用wire.Struct注入,第一個參數固定為new(結構名),後面可接任意多個參數,表示需要為該結構的哪些字段注入值。上面我們需要注入Player和Monster兩個字段。或者我們也可以使用通配符*表示注入所有字段:

<code>var endingASet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingA), "*"))
var endingBSet = wire.NewSet(monsterPlayerSet, wire.Struct(new(EndingB), "*"))
複製代碼/<code>

wire為我們生成正確的代碼,非常棒:

<code>func InitEndingA(name string) EndingA {
player := NewPlayer(name)
monster := NewMonster()
endingA := EndingA{
Player: player,
Monster: monster,
}
return endingA
}
複製代碼/<code>

綁定值

有時候,我們需要為某個類型綁定一個值,而不想依賴構造器每次都創建一個新的值。有些類型天生就是單例,例如配置,數據庫對象(sql.DB)。這時我們可以使用wire.Value綁定值,使用wire.InterfaceValue綁定接口。例如,我們的怪獸一直是一個Kitty,我們就不用每次都去創建它了,直接綁定這個值就 ok 了:

<code>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{}
}
複製代碼/<code>

注意一點,這個值每次使用時都會拷貝,需要確保拷貝無副作用:

<code>// wire_gen.go
func InitEndingA(name string) EndingA {
player := NewPlayer(name)
monster := _wireMonsterValue
endingA := NewEndingA(player, monster)
return endingA
}

var (
_wireMonsterValue = kitty
)
複製代碼/<code>

結構字段作為構造器

有時候我們編寫一個構造器,只是簡單的返回某個結構的一個字段,這時可以使用wire.FieldsOf簡化操作。現在我們直接創建了Mission結構,如果想獲得Monster和Player類型的對象,就可以對Mission使用wire.FieldsOf:

<code>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)
}
複製代碼/<code>

同樣的,第一個參數為new(結構名),後面跟多個參數表示將哪些字段作為構造器,*表示全部。

清理函數

構造器可以提供一個清理函數,如果後續的構造器返回失敗,前面構造器返回的清理函數都會調用:

<code>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
}
複製代碼/<code>

一些細節

首先,我們調用wire生成wire_gen.go之後,如果wire.go文件有修改,只需要執行go generate即可。go generate很方便,我之前一篇文章寫過generate,感興趣可以看看深入理解Go之generate。

總結

wire是隨著go-cloud的示例guestbook一起發佈的,可以閱讀guestbook看看它是怎麼使用wire的。與dig不同,wire只是生成代碼,不使用reflect庫,性能方面是不用擔心的。因為它生成的代碼與你自己寫的基本是一樣的。如果生成的代碼有性能問題,自己寫大概率也會有。


分享到:


相關文章: