Wire
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)
提示
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在依赖注入时,是根据类型匹配来实现的。
ype 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{}) Binding
var 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 string
wire将无法区分这些参数该如何注入,为了避免冲突,可以使用类型别名。