DI(Dependency Injection)是一种设计模式,其中对象所依赖的对象不是自己准备的,而是从外部传递(从外部注入)。
例:考虑一个结构体,它有一个方法,给定一个导演的名字,返回一个包含该导演所有电影的列表:
1 | func (ml *MovieLister) MoviesDirectedBy(director string ) []Movie { |
该结构在名为 finder 的字段上具有 FindAll() 方法
1 | type MovieLister struct { |
此查找器将由 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: go get github.com/google/wire/cmd/wire
准备一个定义依赖项的文件:
1 | //+ wireinject |
重要的//+build wireinject
是第 1 行的构建标记。这将在正常构建期间将 wire.go 从构建中排除。
此外,wire.Build
在函数参数中枚举Provider。wire 检查这些函数的签名以解决依赖关系。
这里使用的Provider如下:
1 | func NewColonDelimitedMovieFinder(fileName string ) MoviesFinder |
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 | func main() { |
请注意,wire.Build()
的参数没有特定顺序。即使改变,生成的结果也不会改变,比如:
1 | wire.Build( |
除了简单地返回值之外,Provider还可以返回错误和函数。例如,如果 NewColonDelimitedMovieFinder 返回错误,则为:
1 | func NewColonDelimitedMovieFinder(fileName string) (MoviesFinder, error) |
相应的initMovieLister
函数也应该返回一个错误
1 | func initMovieLister(fileName string ) (*MovieLister, error ) { |
错误处理也将在生成的代码中完成
1 | func initMovieLister(fileName string ) (*MovieLister, error ) { |
可以在 wire.go 中定义任何 Provider 函数。此处定义的提供程序将复制到 wire_gen.go使用。
例如,转到标准sql.Open()
函数。
1 | func Open(driverName, dataSourceName string ) (*DB, error ) |
由于它不能直接与 wire 一起使用,因此请在 Injector 中为 sql.Open 准备一个包装器。
1 | type DriverName string |
在这定义了自己所需要的类型,使字符串可以按类型区分。如果您的数据库设置以类似 DBConfig 的结构组织,您可以将 DBConfig 作为参数传递给 provideDBConn 函数,并将字段传递给 sql.Open。
1 | var movieListerSet = wire.NewSet( |
1 | wire.Build( |
Set的使用是可选的,其实不使用 Set 也可以解决依赖关系。只是 Set 可用于避免冲突,例如当包名称冲突并需要别名时。
在其原始实现中,NewColonDelimitedMovieFinder 返回 MoviesFinder 接口的值,但返回具体类型 (*ColonDelimitedMovieFinder) 是可以的。 但是,在这种情况下,我们需要使用 wire.Bind() 将 *ColonDelimitedMovieFinder 绑定到 MoviesFinder 接口。
1 | func NewColonDelimitedMovieFinder(fileName string)*ColonDelimitedMovieFinder {} |
1 | wire.Build( |
这确保 *ColonDelimitedMovieFinder 被传递给请求 MoviesFinder 接口的Provider。
引用结构的字段
比如想将 Director.Name 传递给 NewMovie 的param
1 | type Movie struct { |
在这种情况下使用 wire.FieldsOf()
1 | func initMovie() *Movie { |
生成的代码 :
1 | func initMovie() *Movie { |
值和指针:
例如,如果一个 Provider 返回一个指针而另一个 Provider 取一个值,wire 将抛出类似“No provider found for ColonDelimitedMovieFinder”的错误。
1 | func NewColonDelimitedMovieFinder(fileName string ) *ColonDelimitedMovieFinder |
返回值或参数类型错误
通常,go run
有时只需要传递入 main.go,但在使用 wire 时还需要传递 wire_gen.go,然后运行 :
1 | $ go run main.go wire_gen.go |
否则,wire_gen.go 中定义的 Injector 函数将是未定义的,将抛出错误。