Wire 是一個輕巧的 Golang 依賴注入工具。它由 Go Cloud 團隊開發,經過自動生成代碼的方式在編譯期完成依賴注入。Uber 的 dig 、來自 Facebook 的 inject 。他們都經過反射機制實現了運行時依賴注入。php
Wire 生成的代碼與手寫無異。這種方式帶來一系列好處:git
方便 debug,如有依賴缺失編譯時會報錯github
由於不須要 Service Locators, 因此對命名沒有特殊要求golang
避免依賴膨脹。生成的代碼只包含被依賴的代碼,而運行時依賴注入則沒法做到這一點算法
依賴關係靜態存於源碼之中, 便於工具分析與可視化微信
運行go get github.com/google/wire/cmd/wire
以後, wire
命令行工具 將被安裝到 $GOPATH/bin
。
網絡
wire 中的兩個核心概念:Provider 和 Injector:閉包
Provider: 生成組件的普通方法。這些方法接收所需依賴做爲參數,建立組件並將其返回。架構
組件能夠是對象或函數 —— 事實上它能夠是任何類型,但單一類型在整個依賴圖中只能有單一 provider。所以返回 int
類型的 provider 不是個好主意。對於這種狀況, 能夠經過定義類型別名來解決。例如先定義type Category int
,而後讓 provider 返回 Category
類型app
// DefaultConnectionOpt provide default connection optionfunc DefaultConnectionOpt()*ConnectionOpt{...}// NewDb provide an Db objectfunc NewDb(opt *ConnectionOpt)(*Db, error){...}// NewUserLoadFunc provide a function which can load userfunc NewUserLoadFunc(db *Db)(func(int) *User, error){...}
一組業務相關的 provider 時常被放在一塊兒組織成 ProviderSet,以方便維護與切換。
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
Injector: 由wire
自動生成的函數。函數內部會按根據依賴順序調用相關 privoder 。
爲了生成此函數, 咱們在 wire.go
(文件名非強制,但通常約定如此)文件中定義 injector 函數簽名。而後在函數體中調用wire.Build
,並以所需 provider 做爲參數(無須考慮順序)。
因爲wire.go
中的函數並無真正返回值,爲避免編譯器報錯, 簡單地用panic
函數包裝起來便可。不用擔憂執行時報錯, 由於它不會實際運行,只是用來生成真正的代碼的依據。
wire.go
// +build wireinject //上面這個條件編譯tag能夠避免生成代碼重名函數報錯
package main
import "github.com/google/wire"
func UserLoader()(func(int)*User, error){ panic(wire.Build(NewUserLoadFunc, DbSet))}
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
運行 wire
命令將生成 wire_gen.go 文件,其中保存了 injector 函數的真正實現。wire.go 中如有非 injector 的代碼將被原樣複製到 wire_gen.go 中(雖然技術上容許,但不推薦這樣做)。
// Code generated by Wire. DO NOT EDIT.
//go:generate wire//+build !wireinject
package main
import ( "github.com/google/wire")
// Injectors from wire.go:
func UserLoader() (func(int) *User, error) { connectionOpt := DefaultConnectionOpt() db, err := NewDb(connectionOpt) if err != nil { return nil, err } v, err := NewUserLoadFunc(db) if err != nil { return nil, err } return v, nil}
// wire.go:
var DbSet = wire.NewSet(DefaultConnectionOpt, NewDb)
上述代碼有兩點值得關注:
wire.go 第一行
// ***+build\*** wireinject
,這個 build tag 確保在常規編譯時忽略 wire.go 文件(由於常規編譯時不會指定wireinject
標籤)。與之相對的是 wire_gen.go 中的//***+build\*** !wireinject
。兩組對立的 build tag 保證在任意狀況下, wire.go 與 wire_gen.go 只有一個文件生效, 避免了「UserLoader 方法被重複定義」的編譯錯誤自動生成的 UserLoader 代碼包含了 error 處理。與咱們手寫代碼幾乎相同。對於這樣一個簡單的初始化過程, 手寫也不算麻煩。但當組件數達到幾10、上百甚至更多時, 自動生成的優點就體現出來了。
要觸發「生成」動做有兩種方式:go generate
或 wire
。前者僅在 wire_gen.go 已存在的狀況下有效(由於 wire_gen.go 的第三行 //***go:generate\*** wire
),然後者在任什麼時候候都有能夠調用。而且後者有更多參數能夠對生成動做進行微調, 因此建議始終使用 wire
命令。
而後咱們就可使用真正的 injector 了, 例如:
package main
import "log"
func main() {
fn, err := UserLoader()
if err != nil {
log.Fatal(err)
}
user := fn(123)
...
}
若是不當心忘記了某個 provider, wire
會報出具體的錯誤, 幫忙開發者迅速定位問題。例如咱們修改 wire.go
,去掉其中的NewDb
// +build wireinject
package main
import "github.com/google/wire"
func UserLoader()(func(int)*User, error){
panic(wire.Build(NewUserLoadFunc, DbSet))
}
var DbSet = wire.NewSet(DefaultConnectionOpt) //forgot add Db provider
將會報出明確的錯誤:「no provider found for *example.Db
」
wire: /usr/example/wire.go:7:1: inject UserLoader: no provider found for *example.Db
needed by func(int) *example.User in provider "NewUserLoadFunc" (/usr/example/provider.go:24:6)
wire: example: generate failed
wire: at least one generate failure
一樣道理, 若是在 wire.go 中寫入了未使用的 provider , 也會有明確的錯誤提示。
高級功能
談過基本用法之後, 咱們再看看高級功能
*接口注入*
有時須要自動注入一個接口, 這時有兩個選擇:
較直接的做法是在 provider 中生成具體類, 而後返回接口類型。但這不符合Golang 代碼規範。通常不採用
讓 provider 返回具體類,但在 injector 聲明環節做文章,將類綁定成接口,例如:
// FooInf, an interface
// FooClass, an class which implements FooInf
// fooClassProvider, a provider function that provider *FooClassvar set = wire.NewSet(
fooClassProvider,
wire.Bind(new(FooInf), new(*FooClass) // bind class to interface
)
*屬性自動注入*
有時咱們不需什麼特定的初始化工做, 只是簡單地建立一個對象實例, 爲其指定屬性賦值,而後返回。當屬性多的時候,這種工做會很無聊。
// provider.gotype App struct {
Foo *Foo
Bar *Bar
}func DefaultApp(foo *Foo, bar *Bar)*App{
return &App{Foo: foo, Bar: bar}
}
// wire.go
...
wire.Build(provideFoo, provideBar, DefaultApp)
...
wire.Struct
能夠簡化此類工做, 指定屬性名來注入特定屬性:
wire.Build(provideFoo, provideBar, wire.Struct(new(App),"Foo","Bar")
若是要注入所有屬性,則有更簡化的寫法:
wire.Build(provideFoo, provideBar, wire.Struct(new(App), "*")
若是 struct 中有個別屬性不想被注入,那麼能夠修改 struct 定義:
type App struct {
Foo *Foo
Bar *Bar
NoInject int `wire:"-"`
}
這時 NoInject
屬性會被忽略。與常規 provider 相比, wire.Struct
提供一項額外的靈活性:它能適應指針與非指針類型,根據須要自動調整生成的代碼。
你們能夠看到wire.Struct
的確提供了一些便利。但它要求注入屬性可公開訪問, 這致使對象暴露本可隱藏的細節。
好在這個問題能夠經過上面提到的「接口注入」來解決。用 wire.Struct
建立對象,而後將其類綁定到接口上。至於在實踐中如何權衡便利性和封裝程度,則要具體狀況具體分析了。
*值綁定*
雖不常見,但有時須要爲基本類型的屬性綁定具體值, 這時可使用 wire.Value
:
// provider.go
type Foo struct {
X int
}// wire.go
...
wire.Build(wire.Value(Foo{X: 42}))
...
爲接口類型綁定具體值,可使用 wire.InterfaceValue
:
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
*把對象屬性用做 Provider*
有時咱們只是須要用某個對象的屬性做爲 Provider,例如
// provider
func provideBar(foo Foo)*Bar{
return foo.Bar
}
// injector
...
wire.Build(provideFoo, provideBar)
...
這時能夠用 wire.FieldsOf
加以簡化,省掉囉嗦的 provider:
wire.Build(provideFoo, wire.FieldsOf(new(Foo), "Bar"))
與 wire.Struct
相似, wire.FieldsOf
也會自動適應指針/非指針的注入請求
*清理函數*
前面提到若 provider 和 injector 函數有返回錯誤, 那麼 wire 會自動處理。除此之外,wire 還有另外一項自動處理能力:清理函數。
所謂清理函數是指型如 func()
的閉包, 它隨 provider 生成的組件一塊兒返回, 確保組件所需資源能夠獲得清理。
清理函數典型的應用場景是文件資源和網絡鏈接資源,例如:
type App struct {
File *os.File
Conn net.Conn
}
func provideFile() (*os.File, func(), error) {
f, err := os.Open("foo.txt")
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Println(err)
}
}
return f, cleanup, nil
}
func provideNetConn() (net.Conn, func(), error) {
conn, err := net.Dial("tcp", "foo.com:80")
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := conn.Close(); err != nil {
log.Println(err)
}
}
return conn, cleanup, nil
}
上述代碼定義了兩個 provider 分別提供了文件資源和網絡鏈接資源
wire.go
// +build wireinject
package main
import "github.com/google/wire"
func NewApp() (*App, func(), error) {
panic(wire.Build(
provideFile,
provideNetConn,
wire.Struct(new(App), "*"),
))
}
注意因爲 provider 返回了清理函數, 所以 injector 函數簽名也必須返回,不然將會報錯
wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
// Injectors from wire.go:
func NewApp() (*App, func(), error) {
file, cleanup, err := provideFile()
if err != nil {
return nil, nil, err
}
conn, cleanup2, err := provideNetConn()
if err != nil {
cleanup()
return nil, nil, err
}
app := &App{
File: file,
Conn: conn,
}
return app, func() {
cleanup2()
cleanup()
}, nil
}
生成代碼中有兩點值得注意:
當
provideNetConn
出錯時會調用cleanup()
, 這確保了即便後續處理出錯也不會影響前面已分配資源的清理。最後返回的閉包自動組合了
cleanup2()
和cleanup()
。意味着不管分配了多少資源, 只要調用過程不出錯,他們的清理工做就會被集中到統一的清理函數中。最終的清理工做由 injector 的調用者負責
能夠想像當幾十個清理函數的組合在一塊兒時, 手工處理上述兩個場景是很是繁瑣且容易出錯的。wire 的優點再次得以體現。
而後就可使用了:
func main() {
app, cleanup, err := NewApp()
if err != nil {
log.Fatal(err)
}
defer cleanup()
...
}
注意 main 函數中的 defer cleanup()
,它確保了全部資源最終獲得回收
本文分享自微信公衆號 - golang算法架構leetcode技術php(golangLeetcode)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。