go語言wire是 Google 開源的一個依賴注入工具

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

go語言中文網:www.topgoer.com

快速使用

先安裝工具:

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

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

下面我們在一個例子中看看如何使用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} }

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

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這兩行之間是否有空行,這個空行必須要有!見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()都需要傳入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

參考

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


分享到:


相關文章: