Wire
wire 是谷歌開源的一個依賴注入工具,依賴注入這個概念在 Java 的 Spring 框架中相當盛行,go 中也有一些依賴注入庫,例如 Uber 開源的 dig。不過 wire 的依賴注入理念並不是基於語言的反射機制,嚴格來說,wire 其實是一個代碼生成器,依賴注入的理念只體現在使用上,如果有問題的話,在代碼生成期間就能找出來。
倉庫地址:google/wire: Compile-time Dependency Injection for Go (github.com)
文檔地址:wire/docs/guide.md at main · google/wire (github.com)
安裝
安裝代碼生成工具
go install github.com/google/wire/cmd/wire@latest安裝源代碼依賴
go get github.com/google/wire入門
wire 中依賴注入基於兩個元素,provier和injector。
provier可以是開發者提供一個構造器,如下,Provider 必須是對外暴露的。
package foobarbaz
type Foo struct {
X int
}
// 構造Foo
func ProvideFoo() Foo {
return Foo{X: 42}
}帶參數
package foobarbaz
// ...
type Bar struct {
X int
}
// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}也可以帶有參數和返回值
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}也可以對 proiver 進行組合
package foobarbaz
import (
// ...
"github.com/google/wire"
)
// ...
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)TIP
wire 對 provider 的返回值有如下規定
- 第一個返回值是 provider 提供的值
- 第二個返回值必須是
func() | error - 第三個返回值,如果第二個返回值是
func,那麼第三個返回值必須是error
injector是由 wire 生成的一個函數,它負責按照指定的順序去調用 provider,injector 的簽名由開發者來定義,wire 生成具體的函數體,通過調用wire.Build來聲明,這個聲明不應該被調用,更不應該被編譯。
func Build(...interface{}) string {
return "implementation not generated, run wire"
}// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
// 定義的injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}然後執行
wire就會生成wire_gen.go,內容如下
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//+build !wireinject
package main
import (
"example.com/foobarbaz"
)
// 實際生成的injector
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
foo := foobarbaz.ProvideFoo()
bar := foobarbaz.ProvideBar(foo)
baz, err := foobarbaz.ProvideBaz(ctx, bar)
if err != nil {
return foobarbaz.Baz{}, err
}
return baz, nil
}生成的代碼對於 wire 幾乎沒有任何依賴,不需要 wire 也可以正常工作,並且在後續執行go generate就可以再次生成,之後,開發者通過調用實際生成的 injector 傳入對應的參數完成依賴注入。是不是整個過程的代碼相當簡單,感覺好像就是提供幾個構造器,然後生成一個調用構造器的函數,最後再調用這個函數傳入參數,好像也沒做什麼特別復雜的事情,手寫一樣可以,沒錯就是這樣,wire 就是做的這樣一件簡單的事情,只是由手寫變成了自動生成。按照 wire 的理念,依賴注入本就是應該如此簡單的一個事情,不應復雜化。
示例
下面來通過一個案例加深一下理解,這是一個初始化 app 的例子。
HttpServer的 provider 接收一個net.Addr參數,返回指針和error
var ServerProviderSet = wire.NewSet(NewHttpserver)
type HttpServer struct {
net.Addr
}
func NewHttpserver(addr net.Addr) (*HttpServer, error) {
return &HttpServer{addr}, nil
}下面的MysqlClient和System的 provider 同理
var DataBaseProviderSet = wire.NewSet(NewMysqlClient)
type MysqlClient struct {
}
var SystemSet = wire.NewSet(NewApp)
type System struct {
server *HttpServer
data *MysqlClient
}
func (s *System) Run() {
log.Printf("app run on %s", s.server.String())
}
func NewApp(server *HttpServer, data *MysqlClient) (System, error) {
return System{server: server, data: data}, nil
}provider 定義完畢後,需要定義 injector,最好新建一個wire.go文件來定義
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"net"
)
// 定義injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
// 按照順序調用provider
panic(wire.Build(DataBaseProviderSet, ServerProviderSet, SystemSet))
}+build wireinject是為了在編譯時忽略掉此 injector。然後執行如下命令,有如下輸出即生成成功。
$ wire
$ wire: golearn: wrote /golearn/wire_gen.go生成後的代碼如下
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"net"
)
// Injectors from wire.go:
// 定義injector
func initSystemServer(serverAddr net.Addr, dataAddr string) (System, error) {
httpServer, err := NewHttpserver(serverAddr)
if err != nil {
return System{}, err
}
mysqlClient, err := NewMysqlClient(dataAddr)
if err != nil {
return System{}, err
}
system, err := NewApp(httpServer, mysqlClient)
if err != nil {
return System{}, err
}
return system, nil
}可以看到邏輯很清晰,調用順序也是正確的,最後通過生成的 injector 來啟動 app。
package main
import (
"github.com/google/wire"
"log"
"net"
"net/netip"
)
func main() {
server, err := initSystemServer(
net.TCPAddrFromAddrPort(netip.MustParseAddrPort("127.0.0.1:8080")),
"mysql:localhost:3306/test")
if err != nil {
panic(err)
}
server.Run()
}最後輸出如下
2023/08/01 19:20:48 app run on 127.0.0.1:8080這就是一個非常簡單的使用案例。
高級用法
接口綁定
有時候,依賴注入時會將一個具體的實現注入到接口上。wire 在依賴注入時,是根據類型匹配來實現的。
type Fooer interface {
Foo() string
}
type MyFooer string
func (b *MyFooer) Foo() string {
return string(*b)
}
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}
type Bar string
func provideBar(f Fooer) string {
// f will be a *MyFooer.
return f.Foo()
}providerprovideBar的參數是一個接口類型,它的實際上是*MyFooer,為了讓代碼生成時 provider 能夠正確匹配,我們可以將兩種類型綁定,如下
第一個參數是具體的接口指針類型,第二個是具體實現的指針類型。
func Bind(iface, to interface{}) Bindingvar Set = wire.NewSet(
provideMyFooer,
wire.Bind(new(Fooer), new(*MyFooer)),
provideBar)值綁定
在使用wire.Build時,可以不用 provider 提供值,也可以使用wire.Value來提供一個具體的值。wire.Value支持表達式來構造值,這個表達式在生成代碼時會被復制到 injector 中,如下。
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}生成的 injector
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)如果想要綁定一個接口類型的值,可以使用wire.InterfaceValue
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}結構體構造
在 providerset 中,可以使用wire.Struct來利用其他 provider 的返回值構建一個指定類型的結構體。
第一個參數應該傳入結構體指針類型,後續是字段名稱。
func Struct(structType interface{}, fieldNames ...string) StructProvider示例如下
type Foo int
type Bar int
func ProvideFoo() Foo {/* ... */}
func ProvideBar() Bar {/* ... */}
type FooBar struct {
MyFoo Foo
MyBar Bar
}
var Set = wire.NewSet(
ProvideFoo,
ProvideBar,
wire.Struct(new(FooBar), "MyFoo", "MyBar"))
func injectFooBar() FoodBar {
wire.Build(Set)
}生成的 injector 可能如下所示
func injectFooBar() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}如果想要填充所有字段,可以使用*,例如
wire.Struct(new(FooBar), "*")默認是構造結構體類型,如果想要構造指針類型,可以修改 injector 簽名的返回值
func injectFooBar() *FoodBar {
wire.Build(Set)
}如果想要忽略掉字段,可以加 tag,如下所示
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}Cleanup
如果 provider 構造的一個值在使用後需要進行收尾工作(比如關閉一個文件),provider 可以返回一個閉包來進行這樣的操作,injector 並不會調用這個 cleanup 函數,具體何時調用交給 injector 的調用者,如下。
type Data struct {
// TODO wrapped database client
}
// NewData .
func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
cleanup := func() {
log.NewHelper(logger).Info("closing the data resources")
}
return &Data{}, cleanup, nil
}實際生成的代碼可能如下
func wireApp(confData *conf.Data, logger log.Logger) (func(), error) {
dataData, cleanup, err := data.NewData(confData, logger)
if err != nil {
return nil, nil, err
}
// inject data
// ...
return app, func() {
cleanup()
}, nil
}類型重復
provider 的入參最好不要類型重復,尤其是對於一些基礎類型
type FooBar struct {
foo string
bar string
}
func NewFooBar(foo string, bar string) FooBar {
return FooBar{
foo: foo,
bar: bar,
}
}
func InitializeFooBar(a string, b string) FooBar {
panic(wire.Build(NewFooBar))
}這種情況下生成代碼會報錯
provider has multiple parameters of type stringwire 將無法區分這些參數該如何注入,為了避免沖突,可以使用類型別名。
