| by suyi | No comments

Go 的 DI 库 google/wire

什么是DI

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

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

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() 方法

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

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

//+ 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如下:

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等中可以正常调用

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

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

wire.Build( 
        NewColonDelimitedMovieFinder, 
        NewMovieLister, 
    )

Provider错误处理

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

func NewColonDelimitedMovieFinder(fileName string) (MoviesFinder, error)

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

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

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

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()函数。

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

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

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

var movieListerSet = wire.NewSet(
	NewMovieLister,
	NewColonDelimitedMovieFinder,
)
wire.Build(
	movieListerSet,
)

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

接口绑定

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

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

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

引用结构的字段

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

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()

func initMovie() *Movie {
	wire.Build(
		NewMovie,
		NewDirector,
		wire.FieldsOf(new(*Director), "Name"),
	)
	return nil
}

生成的代码 :

func initMovie() *Movie {
    director := NewDirector()
    string2 := director.Name
    movie := NewMovie(string2)
    return movie
}

注意事项

值和指针:

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

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

返回值或参数类型错误

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

$ go run main.go wire_gen.go

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

发表评论