Go 的 DI 库 google/wire

什么是DI

DI(Dependency Injection)是一种设计模式,其中对象所依赖的对象不是自己准备的,而是从外部传递(从外部注入)。

例:考虑一个结构体,它有一个方法,给定一个导演的名字,返回一个包含该导演所有电影的列表:

1
2
3
4
5
6
7
8
9
10
func (ml *MovieLister) MoviesDirectedBy(director string ) []Movie {
allMovies := ml.finder.FindAll()
result := make ([]Movie, 0 , len (allMovies))
for _, m := range allMovies {
if director == m.Director {
result = append (result, m)
}
}
return result
}

该结构在名为 finder 的字段上具有 FindAll() 方法

1
2
3
4
5
6
type MovieLister struct {
finder MoviesFinder
}
type MoviesFinder interface {
FindAll() []Movie
}

此查找器将由 MovieLister 在正常控制流中初始化和设置。

func NewMovieLister() *MovieLister {
return &MovieLister{
finder: NewColonDelimitedMovieFinder( “movies.txt” ),
}
}

由此,MovieLister 与特定的 Finder 紧密结合。为了能够检索数据,无论它是在 RDB 中还是在外部 API 中,都需要在 MovieLister 外部初始化 Finder 并将其传递给 MovieLister。

func NewMovieLister(finder MoviesFinder) *MovieLister {
return &MovieLister{
finder: finder,
}
}

func main() {
finder := NewColonDelimitedMovieFinder( “movies.txt” )
ml := NewMovieLister(finder)
fmt.Println(ml.MoviesDirectedBy( “詹姆斯·卡梅隆” )
}

这样,使用 DI 模式的好处是让代码依赖关系更清晰,更灵活。

wire的使用方法

本地安装wire: go get github.com/google/wire/cmd/wire

准备一个定义依赖项的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
//+ wireinject

package main

import "github.com/google/wire"

func initMovieLister(fileName string ) *MovieLister {
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
return nil
}

重要的//+build wireinject是第 1 行的构建标记。这将在正常构建期间将 wire.go 从构建中排除。

此外,wire.Build在函数参数中枚举Provider。wire 检查这些函数的签名以解决依赖关系。

这里使用的Provider如下:

1
2
func NewColonDelimitedMovieFinder(fileName string ) MoviesFinder
func NewMovieLister(finder MoviesFinder) *MovieLister

wire 检查这些函数的签名并生成代码来解析所需的依赖关系。对于生成,请使用安装go get再.wire

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func initMovieLister(fileName string) *MovieLister {
moviesFinder := NewColonDelimitedMovieFinder(fileName)
movieLister := NewMovieLister(moviesFinder)
return movieLister
}

这样生成的initMovieLister在main等中可以正常调用

1
2
3
4
func main() {
ml := initMovieLister( "movies.txt" )
fmt.Println(ml.MoviesDirectedBy( "詹姆斯·卡梅隆" ))
}

请注意,wire.Build()的参数没有特定顺序。即使改变,生成的结果也不会改变,比如:

1
2
3
4
wire.Build( 
NewColonDelimitedMovieFinder,
NewMovieLister,
)

Provider错误处理

除了简单地返回值之外,Provider还可以返回错误和函数。例如,如果 NewColonDelimitedMovieFinder 返回错误,则为:

1
func NewColonDelimitedMovieFinder(fileName string) (MoviesFinder, error)

相应的initMovieLister函数也应该返回一个错误

1
2
3
4
5
6
7
func initMovieLister(fileName string ) (*MovieLister, error ) {
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
return nil , nil
}

错误处理也将在生成的代码中完成

1
2
3
4
5
6
7
8
func initMovieLister(fileName string ) (*MovieLister, error ) {
moviesFinder, err := NewColonDelimitedMovieFinder(fileName)
if err != nil {
return nil , err
}
movieLister := NewMovieLister(moviesFinder)
return movieLister, nil
}

对应的Injector

可以在 wire.go 中定义任何 Provider 函数。此处定义的提供程序将复制到 wire_gen.go使用。

例如,转到标准sql.Open()函数。

1
func Open(driverName, dataSourceName string ) (*DB, error )

由于它不能直接与 wire 一起使用,因此请在 Injector 中为 sql.Open 准备一个包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
type DriverName string
type DataSourceName string

func provideDBConn(driver DriverName, dsn DataSourceName) (*sql.DB, error) {
return sql.Open(string(driver), string(dsn))
}

func initDBConn(driver DriverName, dsn DataSourceName) (*sql.DB, error) {
wire.Build(
provideDBConn,
)
return nil, nil
}

在这定义了自己所需要的类型,使字符串可以按类型区分。如果您的数据库设置以类似 DBConfig 的结构组织,您可以将 DBConfig 作为参数传递给 provideDBConn 函数,并将字段传递给 sql.Open。

Provider Set

1
2
3
4
var movieListerSet = wire.NewSet(
NewMovieLister,
NewColonDelimitedMovieFinder,
)
1
2
3
wire.Build(
movieListerSet,
)

Set的使用是可选的,其实不使用 Set 也可以解决依赖关系。只是 Set 可用于避免冲突,例如当包名称冲突并需要别名时。

接口绑定

在其原始实现中,NewColonDelimitedMovieFinder 返回 MoviesFinder 接口的值,但返回具体类型 (*ColonDelimitedMovieFinder) 是可以的。 但是,在这种情况下,我们需要使用 wire.Bind() 将 *ColonDelimitedMovieFinder 绑定到 MoviesFinder 接口。

1
2
func NewColonDelimitedMovieFinder(fileName string)*ColonDelimitedMovieFinder {}
func NewMovieLister(finder MoviesFinder) *MovieLister {}
1
2
3
4
5
wire.Build(
NewMovieLister,
NewColonDelimitedMovieFinder,
wire.Bind(new(MoviesFinder), new(*ColonDelimitedMovieFinder)),
)

这确保 *ColonDelimitedMovieFinder 被传递给请求 MoviesFinder 接口的Provider。

引用结构的字段

比如想将 Director.Name 传递给 NewMovie 的param

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Movie struct {
Director string
}

func NewMovie(director string) *Movie {
return &Movie{Director: director}
}

type Director struct {
Name string
}

func NewDirector(name string) *Director {
return &Director{Name: name}
}

在这种情况下使用 wire.FieldsOf()

1
2
3
4
5
6
7
8
func initMovie() *Movie {
wire.Build(
NewMovie,
NewDirector,
wire.FieldsOf(new(*Director), "Name"),
)
return nil
}

生成的代码 :

1
2
3
4
5
6
func initMovie() *Movie {
director := NewDirector()
string2 := director.Name
movie := NewMovie(string2)
return movie
}

注意事项

值和指针:

例如,如果一个 Provider 返回一个指针而另一个 Provider 取一个值,wire 将抛出类似“No provider found for ColonDelimitedMovieFinder”的错误。

1
2
func NewColonDelimitedMovieFinder(fileName string ) *ColonDelimitedMovieFinder
func NewMovieLister(finder ColonDelimitedMovieFinder) *MovieLister

返回值或参数类型错误

通常,go run有时只需要传递入 main.go,但在使用 wire 时还需要传递 wire_gen.go,然后运行 :

1
$ go run main.go wire_gen.go

否则,wire_gen.go 中定义的 Injector 函数将是未定义的,将抛出错误。