最近,在负责一个 Go 项目开发。项目分层为:handler 层、service 层 dao 层。其中 handler 层依赖 service 层,service 层依赖不同基础组件以及 dao 层。这样,只要 handler 层、service 层的依赖项发生了变化,对应层的代码都得重新调整,非常繁琐。为此,在网上搜索了 Go 依赖注入相关的资料,得出,常用的 Go 依赖注入框架有:dig、inject、wire。其中 dig、inject 是在运行时注入依赖,对代码有一定的侵入性。而 wire 是代码生成工具,生成的内容也是普通的 Go 代码,只不过,在运行时不再依赖 wire。
本文主要是在自己使用 wire 后,对 wire 用户指南进行完整阅读翻译并记录。
wire 简介
Wire 是一种代码生成工具,其使用依赖注入自动连接组件。
函数参数
NewB(a A) B
wire 安装
go get github.com/google/wire/cmd/wire
确保 $GOPATH/bin 已添加到 $PATH 环境变量。
wire 基础概念
在 wire 中,有两个核心概念:Providers、Injectors。
Providers
Provider
这些函数都是常规的 go 代码。
package foobarbaz
type Foo struct {
X int
}
// ProvideFoo 函数返回一个 Foo 结构体值。
func ProvideFoo() Foo {
return Foo{X: 42}
}
Providers 可以通过参数指定依赖项:
package foobarbaz
type Bar struct {
X int
}
// ProvideBar 函数通过参数指定依赖 Foo 结构体。
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}
Providers 还可以返回 errors:
package foobarbaz
import (
"context"
"errors"
)
type Baz struct {
X int
}
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
}
ProviderSet
package foobarbaz
import (
"github.com/google/wire"
)
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
另外,还可以将多个 ProviderSet 组合起来,形成一个新的 ProviderSet:
package foobarbaz
import (
"example.com/some/other/pkg"
)
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)
Injectors
Injector
一个应用程序就是通过 Injector 将不同的 Providers 串联起来。
使用 Wire 编写 Injector 签名,然后运行 Wire 命令,生成函数主体。
// +build wireinject
// 此 build tag 用于确保此代码文件在最终构建阶段不会被纳入构建。
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}
Injectors 也支持通过参数指定依赖(参数会转交给对应的 Provider),并且也可以返回 error 类型值。wire.Build 的参数与 wire.NewSet 作用一样:它们形成 ProviderSet。这是在生成该 Injector 代码期间使用的 ProviderSet。
在 Injector 代码文件中,找到的所有 Injector 声明,都将被复制到生成的代码文件中。
在 Injector 代码文件目录下,运行 Wire 命令:
wire
wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package main
import (
"example.com/foobarbaz"
)
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 的依赖很小(所有生成的内容都是常规的 Go 代码,无需 Wire 即可使用)。
一旦 wire_gen.go 被创建过,后续,可以直接通过运行 go generate 来重新生成。
Wire 高级特性
以下特性都是建立在 Providers 和 Injectors 概念基础之上。
绑定接口
接口类型的具体实现
Wire 是通过类型标识将输入与输出进行匹配,一般,倾向于创建一个返回接口类型的 Provider。然而,这不是惯用用法,因为 Go 最佳实践是:返回具体实现。
依赖接口,返回(接口的具体)实现。
ProviderSet接口绑定
type Fooer interface {
Foo() string
}
type MyFooer string
// Fooer 接口的具体实现
func (b *MyFooer) Foo() string {
return string(*b)
}
// 返回的是 Fooer 接口的具体实现
func provideMyFooer() *MyFooer {
b := new(MyFooer)
*b = "Hello, World!"
return b
}
type Bar string
// 依赖 Fooer 接口
func provideBar(f Fooer) string {
return f.Foo()
}
var Set = wire.NewSet(
provideMyFooer,
// 接口绑定,其实就是声明 *MyFooer 是 Fooer 接口类型。
wire.Bind(new(Fooer), new(*MyFooer)),
// 这样,provideMyFooer 返回的 *MyFooer 就可以被当成 Fooer 接口类型,注入到 provideBar 中。
provideBar,
)
wire.Bind 函数:
- 第一个参数:所需.接口类型的值.的指针。
- 第二个参数:接口类型的具体实现.的类型.的值.的指针。
结构体 Providers
可以使用 Providers 返回的不同类型,来构造不同的结构体。
wire.Struct
Injector 将使用每个字段类型对应的 Provider 填充每个字段。
Swire.StructS*S
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() FooBar {
foo := ProvideFoo()
bar := ProvideBar()
fooBar := FooBar{
MyFoo: foo,
MyBar: bar,
}
return fooBar
}
wire.Struct
*wire.Struct(new(FooBar),"*")
MyFoo
var Set = wire.NewSet(
ProvideFoo,
wire.Struct(new(FooBar), "MyFoo"))
生成代码如下:
func injectFooBar() FooBar {
foo := ProvideFoo()
fooBar := FooBar{
MyFoo: foo,
}
return fooBar
}
*FooBar
func injectFooBar() *FooBar {
foo := ProvideFoo()
fooBar := &FooBar{
MyFoo: foo,
}
return fooBar
}
wire:"-"*
type Foo struct {
mu sync.Mutex `wire:"-"`
Bar Bar
}
当使用 wire.Struct(new(Foo), "*") 构造 Foo 类型时,Wire 将忽略 mu 字段。
此外,在 wire.Struct(new(Foo), "mu") 中显式指定一个忽略的字段,也是错误的。
绑定 Values
有时,将基本值(通常为 nil)绑定到类型很有用。
不是让 Injector 依赖一次性 Provider 函数
type Foo struct {
X int
}
func injectFoo() Foo {
wire.Build(wire.Value(Foo{X: 42}))
return Foo{}
}
生成代码如下:
func injectFoo() Foo {
foo := _wireFooValue
return foo
}
var (
_wireFooValue = Foo{X: 42}
)
注意,该表达式被复制到 Injector 的函数中。对变量的引用将在 Injector 包装初始化时计算。如果表达式调用任何函数或从任何通道接收,则 Wire 将报错。
对于接口类型 values,使用 InterfaceValue:
func injectReader() io.Reader {
wire.Build(wire.InterfaceValue(new(io.Reader), os.Stdin))
return nil
}
将结构体字段作为 Providers
有时,用户想要的 Providers 是结构体的某些字段。
getS
type Foo struct {
S string
N int
F float64
}
func getS(foo Foo) string {
// 不好的方式!使用 wire.FieldsOf 代替。
return foo.S
}
func provideFoo() Foo {
return Foo{ S: "Hello, World!", N: 1, F: 3.14 }
}
func injectedMessage() string {
wire.Build(
provideFoo,
getS)
return ""
}
可以改用 wire.FieldsOf 来直接使用这些字段,而无需编写 getS 函数:
func injectedMessage() string {
wire.Build(
provideFoo,
wire.FieldsOf(new(Foo), "S"))
return ""
}
生成代码如下:
func injectedMessage() string {
foo := provideFoo()
string2 := foo.S
return string2
}
按需将任意多个字段名称添加到 wire.FieldsOf 函数中。
对于给定的字段类型 T,FieldsOf 至少提供 T;如果 struct 参数是指向结构的指针,则 FieldsOf还提供 *T。
Cleanup 函数
如果 Provider 创建了一个需要清除的值(例如:关闭文件),那么,它可以返回一个闭包函数以清除资源。
如果在 Injector 的实现中稍后调用的 Provider 返回错误,则 Injector 将使用此函数将聚合的清除函数返回给调用者或清理资源。
func provideFile(log Logger, path Path) (*os.File, func(), error) {
f, err := os.Open(string(path))
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := f.Close(); err != nil {
log.Log(err)
}
}
return f, cleanup, nil
}
确保在 Provider 的任何输入的清除功能之前调用清除功能,并且该清除功能必须具有签名 func()。
另一种 Provider 语法
可以使用 panic 来编写更简洁的 Provider:
func injectFoo() Foo {
panic(wire.Build(/* ... */))
}