深入解析golang几种非常主流的依赖注入框架,附实现案例及原理解析

什么是依赖注入?
依赖注入 ,英文全名是 dependency injection,简写为 DI。

百科解释:
依赖注入是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。

依赖注入是一个经典的设计模式,可有效地解决项目中复杂的对象依赖关系。

对于有反射功能的语言来说,实现依赖注入都比较方便一些。在 Golang 中有几个比较知名的依赖注入开源库,例如 google/wire、uber-go/dig 以及 facebookgo/inject 等。


在用编程语言编写程序时,比如用 java 语言,会编写很多类,这些类之间相互调用,完成一个具体的功能。

例如,从 MySQL 获取数据,那么需要一个 MySQL 操作类 。

第一次编写mysql操作类:

class MySQL{
    
    
}

要从 mysql 获取数据,那么 mysql 数据库的用户名,密码,地址等等这些配置信息,也是需要的,继续编写 MySQL 类:

package com.demo.mysql

class MySQL {
    
    
    getMySQLConfig() {
    
    
        port = 3306;
        username = "xxx";
        password = "xxx";
    }
    
    initMySQL(){
    
    }
   
    querySQL(){
    
    }
}

进一步思考,上面的 MySQL 操作类程序有什么不妥的地方?

编程原则里有一个原则就是:单一职责

也就是说一个类最好只干一件事情。

根据这个原则在看看 MySQL 类,里面有获取数据库配置数据,也有操作MySQL的方法,不是单一职责的。
那里面获取数据库配置数据,可不可以单独拎出来用一个类表示? 当然可以。

因为 MySQL 配置数据,多数是从文件里读取的,上面 MySQL 类是写死,这也是不合理的一个地方。
而配置文件的来源,可以是 yml 格式文件,也可以是 toml 格式文件,还可以是远程文件。

第二次编写mysql操作类:
修改上面的类,增加一个获取数据库配置的类:

package com.demo.mysql

class MySQLConfig {
    
    
      getMySQLConfig() {
    
    
        // 从配置文件获取 mysql 配置数据
    }
}

获取数据的类变成:

package com.demo.mysql

class MySQL {
    
    
    initMySQL(){
    
    
     // 获取数据库的配置信息
     mysqlconfig = new MySQLConfig();
    }
   
    querySQL(){
    
    }
}

思考一下,上面改写后的类有什么不妥的地方?
获取mysql的配置信息,是不是要在 MySQL 类里 new一下, 实例化一下,如果不在同一个包下,还要把配置类引入进来在才能实例化。这里能不能优化下,当然可以。

直接把数据库配置类注入到 MySQL 操作类里。这就是依赖注入。

依赖是什么?注入又是什么?
mysql 操作类依赖谁?依赖数据库配置类。
注入什么?把数据库配置类注入到 mysql 操作类里。
注入是一个动作,把一个类注入到另外一个类。
依赖是一种关系,类关系,一个类要完全发挥作用,需要依赖另外一个类。
要完成数据操作,mysql操作类是需要依赖数据库配置类的,把数据库配置类注入到mysql操作类里,就可以完成操作类功能。

第三次编写mysql操作类:
伪代码示例:

package com.demo.mysql

class MySQL {
    
    
    private MySQLConfig config
    MySQL(MySQLConfig mysqlconfig) {
    
     // 数据库配置类这里注入到mysql操作类里
        config = mysqlconfig
    }
    initMySQL(){
    
    
    
    }
   
    querySQL(){
    
    }
}

把数据库配置类注入到mysql操作类里。

写 java 的人都知道 java 框架里有一个 spring 全家桶,spring 框架包核心有2个,其中有一个核心就是 IoC,另一个是 aop。

IoC 的全称:Inversion of Control,控制反转。

这个控制反转也是面向对象编程原则之一。

但是这个控制反转比较难理解,如果结合上面的 DI 来理解,就比较容易理解点。
可以把 DI 看作是 IoC 编程原则的一个具体实现。

依赖注入还可以从另外的软件设计思想来理解:
1)分离关注点
2)高内聚,低耦合

对数据库 mysql 的操作和 mysql 的配置信息,这个 2 个是可以相互独立,相分离的。

何时使用依赖注入
当你的项目规模不大,文件不是很多,一个文件调用只需要传入少量依赖对象时,这时使用依赖注入就会使程序变得繁琐。

当规模变大,单个对象使用需要调用多个依赖对象时,而这些依赖又有自己依赖对象,这时对象创建变得繁琐,那么这时候依赖注入就可以出场了。

wire 概念说明

wire 简介
wire 是由 google 开源的一个用 Go 语言实现的依赖注入代码生成工具。它能够根据你写的代码生成相应的依赖注入 Go 代码。

与其他依赖注入工具不同,比如 uber 的 dig 和 facebook 的 inject,这 2 个工具都是使用反射实现的依赖注入,而且是运行时注入(runtime dependency injection)。

wire 是编译代码时生成代码的依赖注入,是编译期间注入依赖代码(compile-time dependency injection)。而且代码生成期间,如果依赖注入有问题,生成依赖代码时就会出错,就可以报出问题来,而不必等到代码运行时才暴露出问题。

provider 和 injector
首先,需要理解 wire 的 2 个核心概念:provider 和 injector。

从上面 java 模拟依赖注入的例子中,可以简化出依赖注入的步骤:

第一:需要 New 出一个类实例
第二:把这个 New 出来的类实例通过构造函数或者其他方式“注入”到需要使用它的类中
第三:在类中使用这个 New 出来的实例

从上面步骤来理解 wire 的 2 个核心概念 provider 和 injector。

provider 就相当于上面 New 出来的类实例。
injector 就相当于“注入”动作前,把所需依赖函数进行聚合,根据这个聚合的函数生成依赖关系。

provider:提供一个对象。
injector:负责根据对象依赖关系,生成新程序。

provider
provider 是一个普通的 Go 函数 ,可以理解为是一个对象的构造函数。为下面生成 injector 函数提供”构件“。

看下面例子,来自 go blog。

这篇 blog 是 2018.10.9 发表,可能一些信息有点老,再参考 github guide ,这篇 guide 最后更新于 2021.1.26。

下面的 NewUserStore() 函数可以看作是一个 provider。这个函数需要传入 *Config 和 *mysql.DB 2 个参数。

// NewUserStore 是一个 provider for *UserStore,*UserStore 依赖 *Config,*mysql.DB
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {
    
    ... ...}

// NewDefaultConfig 是一个 provider for *Config,没有任何依赖
func NewDefaultConfig() *Config {
    
    ...}

// NewDB 是 *mysql.DB 的一个 provider ,依赖于数据库连接信息 *ConnectionInfo
func NewDB(info *ConnectionInfo) (*mysql.DB, error){
    
    ...}

provider 可以组合成一组 provider set。对于经常在一起使用的 providers 来说,这个非常有用。使用 wire.NewSet 方法可以把他们组合在一起:

var SuperSet = wire.NewSet(NewUserStore, NewDefaultConfig)

也可以把其他的 provider sets 加入一个 provider set :

import (
    “example.com/some/other/pkg”
)

// ... ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

wire.NewSet() 函数:

这个函数可以把相关的 provider 组合在一起然后使用。当然也可以单独使用,如 var Provider = wire.NewSet(NewDB)。

这个 NewSet 函数的返回值也可以作为其他 NewSet 函数的参数使用,比如上面的 SuperSet 作为参数使用。

injector
我们编写程序把这些 providers 组合起来(比如下面例子 initUserStore() 函数),wire 里的 wire 命令会按照依赖顺序调用 providers 生成更加完整的函数,这个就是 injector。

首先,编写生成 injector 的签名函数,然后用 wire 命令生成相应的函数。

例子如下:

// +build wireinject

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
    
    
    wire.Build(SuperSet, NewDB) // 声明获取 UserStore 需要调用哪些 provider 函数
    return nil, nil
}

然后用 wire 命令把上面的 initUserStore 函数生成 injector 函数,生成的函数对应文件名 wire_gen.go。

wire 命令:

You can generate the injector by invoking Wire in the package directory。

直接在生成 injector 函数的包下,使用 wire 命令,就可以生成 injector 代码。

wire.Build() 函数:

它的参数可以是 wire.NewSet() 组织的一个或多个 provider,也可以直接使用 provider。

wire 使用
wire 结构体和方法列表:

func Build(...interface{
    
    }) string
type Binding
	func Bind(iface, to interface{
    
    }) Binding
type ProvidedValue
	func InterfaceValue(typ interface{
    
    }, x interface{
    
    }) ProvidedValue
	func Value(interface{
    
    }) ProvidedValue
type ProviderSet
	func NewSet(...interface{
    
    }) ProviderSet
type StructFields
	func FieldsOf(structType interface{
    
    }, fieldNames ...string) StructFields
type StructProvider
	func Struct(structType interface{
    
    }, fieldNames ...string) StructProvider

wire 安装

go get github.com/google/wire/cmd/wire

快速开始
例子1
先新建一个 basics 的文件夹,然后在 basics 里使用 go mod init basics,新建一个 go.mod,在 go.mod 里引入 wire:require github.com/google/wire v0.5.0。

整个文件夹目录结构:
在这里插入图片描述
定义 providers
在 basics 文件夹下新建 basics.go 文件,写入如下代码:

package main

import (
	"context"
	"errors"
)

type Student struct {
    
    
	ClassNo int
}

// NewStudent 就是一个 provider,返回一个 Student
func NewStudent() Student {
    
    
	return Student{
    
    ClassNo: 10}
}

type Class struct {
    
    
	ClassNo int
}

// NewClass 就是一个 provider,返回一个 Class
func NewClass(stu Student) Class {
    
    
	return Class{
    
    ClassNo: stu.ClassNo}
}

type School struct {
    
    
	ClassNo int
}

// NewSchool 是一个 provider,返回一个 School
// 与上面 provider 不同的是,它还返回了一个错误信息
func NewSchool(ctx context.Context, class Class) (School, error) {
    
    
	if class.ClassNo == 0 {
    
    
		return School{
    
    }, errors.New("cannot provider school when class is 0")
	}
	return School{
    
    ClassNo: class.ClassNo}, nil
}

定义 injector
新建文件 wire.go,代码如下:

// +build wireinject

package main

import (
	"github.com/google/wire"
)

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

func initSchool() (School, error) {
    
    
	wire.Build(SuperSet)
	return School{
    
    }, nil
}

// +build wireinject ,这一行代码一定要在包最上面声明,表明这是一个准备被编译的 injector

用 wire 命令生成 injector 函数代码
用 wire 命令生成 injector 代码,在 basics 目录下执行 wire 命令:

$ wire
wire: D:\work\mygo\go-practice2\di\wire\basics\wire.go:9:1: inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet" (D:\work\mygo\go-practice2\di\wire\basics\wire.go:7:16)

wire: basics: generate failed
wire: at least one generate failure

报错了,看看显示出的错误信息,最主要是这一行信息:

inject initSchool: no provider found for context.Context needed by basics.School in provider set "SuperSet"

来看一看 initSchool 函数,果然没有给它提供 context.Context 。我们来修改函数,引入 context 包,然后给 initSchool 函数增加参数 context.Context:

func initSchool(ctx context.Context) (School, error) {
    
    
	wire.Build(SuperSet)
	return School{
    
    }, nil
}

再来用命令 wire 编译:

$ wire
wire: basics: wrote D:\work\mygo\go-practice2\di\wire\basics\wire_gen.go

生成的 injector 代码,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 (
	"context"
	"github.com/google/wire"
)

// Injectors from wire.go:

func initSchool(ctx context.Context) (School, error) {
    
    
	student := NewStudent()
	class := NewClass(student)
	school, err := NewSchool(ctx, class)
	if err != nil {
    
    
		return School{
    
    }, err
	}
	return school, nil
}

// wire.go:

var SuperSet = wire.NewSet(NewStudent, NewClass, NewSchool)

wire 使用的步骤:

先编写 provider。
再编写 injector:把相关 provider 组织在一起,成为一个 ProviderSet。
最后用 wire 命令编译:wire 会根据 provider 之间相关依赖生成代码。
wire.NewSet 函数:

它可以把 provider 集合起来。作用1分类:可以把一组相关的 provider 写在一起组成 ProviderSet。作用1延伸第2个作用,避免 provider 过多难于管理。

wite.Build 函数:

func Build(...interface{
    
    }) string

它的参数是 provider 不定长列表。 把所有相关的 provider 组织在一起然后生成 injector 函数代码。它是生成 injector 函数的模板函数。

绑定接口#
上面例子1绑定的是结构体和构造函数。如果有接口 interface 参与呢,那怎么办?比如下面的代码,

type Fooer interface {
    
    
    Hello()
}

type Foo struct{
    
    }

func (f Foo)Hello() {
    
    
    fmt.Println("hello")
}

func Bar struct{
    
    }

func NewBar() Bar {
    
    
    return Bar{
    
    }
}

有接口 Fooer,这个怎么绑定呢?这时候就可以用 [wire.Bind](wire/wire.go at v0.5.0 · google/wire · GitHub) 函数:

var bind = wire.Bind(new(Fooer), new(Foo))
var set = wire.NewSet(bind, NewBar)

// or
var set = wire.NewSet(wire.Bind(new(Fooer), new(Foo)), NewBar)

struct prividers
struct 也可以直接当作一个 provider 使用。如果结构体的 provider 仅仅是用作字段赋值,那么可以使用函数 wire.Struct 来赋值。

type Foo int
type Bar int

func NewFoo() Foo {
    
    /* ... */}
func NewBar() Bar {
    
    /* ... */}

type FooBar struct {
    
    
    MyFoo Foo
    MyBar Bar
}

var set = wire.NewSet(
	NewFoo,
    NewBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"),
)

更多资料查看:
https://github.com/google/wire
https://github.com/google/wire/blob/main/docs/guide.md
https://go.dev/blog/wire
https://pkg.go.dev/github.com/google/wire#pkg-index


基于 facebookgo/inject 的依赖注入,着重讨论以下几点内容:

依赖注入出现的背景以及解决的问题
facebookgo/inject 的使用方法
facebookgo/inject 的缺陷
依赖注入的背景
对于稍微复杂些的项目,我们往往就会遇到对象之间复杂的依赖关系。手动管理和初始化这些管理关系将会极其繁琐,依赖注入可以帮我们自动实现依赖的管理和对象属性的赋值,将我们从这些繁琐的依赖管理中解放出来。

以一个常见的 HTTP 服务为例,我们在开发后台时往往会把代码分为 Controller、Service 等层次。如下:

type UserController struct {
    
    
	UserService *UserService
	Conf        *Conf
}

type PostController struct {
    
    
	UserService *UserService
	PostService *PostService
	Conf        *Conf
}

type UserService struct {
    
    
	Db   *DB
	Conf *Conf
}

type PostService struct {
    
    
	Db *DB
}

type Server struct {
    
    
    UserApi *UserController
    PostApi *PostController
}

上述的代码例子中,有两个 Controller:UserController 和 PostController,分别用来接收用户和文章的相关请求逻辑。除此之外还会有 Service 相关类、Conf 配置文件、DB 连接等。

这些对象之间存在比较复杂的依赖关系,这就给项目的初始化带来了一些困扰。对于以上代码,对应初始化逻辑大概就会是这样:

func main() {
    
    
	conf := loadConf()
	db := connectDB()

	userService := &UserService{
    
    
		Db:   db,
		Conf: conf,
	}

	postService := &PostService{
    
    
		Db: db,
	}

	userHandler := &UserController{
    
    
		UserService: userService,
		Conf:        conf,
	}

	postHandler := &PostController{
    
    
		UserService: userService,
		PostService: postService,
		Conf:        conf,
	}

	server := &Server{
    
    
		UserApi: userHandler,
		PostApi: postHandler,
	}

	server.Run()
}

有一大段的逻辑都是用来做对象初始化,而当接口越来越多的时候,整个初始化过程就会异常的冗长和复杂。

针对以上问题,依赖注入可以完美地解决。

facebookgo/inject 的使用
接下来,我们试着使用 facebookgo/inject 的方式,对这段代码进行依赖注入的改造。如下:

type UserController struct {
    
    
	UserService *UserService `inject:""`
	Conf        *Conf        `inject:""`
}

type PostController struct {
    
    
	UserService *UserService `inject:""`
	PostService *PostService `inject:""`
	Conf        *Conf        `inject:""`
}

type UserService struct {
    
    
	Db   *DB   `inject:""`
	Conf *Conf `inject:""`
}

type PostService struct {
    
    
	Db *DB `inject:""`
}

type Server struct {
    
    
	UserApi *UserController `inject:""`
	PostApi *PostController `inject:""`
}

func main() {
    
    
	conf := loadConf() // *Conf
	db := connectDB() // *DB

	server := Server{
    
    }

	graph := inject.Graph{
    
    }

	if err := graph.Provide(
		&inject.Object{
    
    
			Value: &server,
		},
		&inject.Object{
    
    
			Value: conf,
		},
		&inject.Object{
    
    
			Value: db,
		},
	); err != nil {
    
    
		panic(err)
	}

	if err := graph.Populate(); err != nil {
    
    
		panic(err)
	}

	server.Run()
}

首先每一个需要注入的字段都需要打上 inject:“” 这样的 tag。所谓依赖注入,这里的依赖指的就是对象中包含的字段,而注入则是指有其它程序会帮你对这些字段进行赋值。

其次,我们使用 inject.Graph{} 创建一个 graph 对象。这个 graph 对象将负责管理和注入所有的对象。至于为什么叫 Graph,其实这个名词起的非常形象,因为各个对象之间的依赖关系,也确实像是一张图一样。

接下来,我们使用 graph.Provide() 将需要注入的对象提供给 graph。

graph.Provide(
	&inject.Object{
    
    
		Value: &server,
	},
	&inject.Object{
    
    
		Value: &conf,
	},
	&inject.Object{
    
    
		Value: &db,
	},
);

最后调用 Populate 函数,开始进行注入。

从代码中可以看到,我们一共就向 Graph 中 Provide 了三个对象。我们提供了 server 对象,是因为它是一个顶层对象。提供了 conf 和 db对象,是因为所有的对象都依赖于它们,可以说它们是基础对象了。

但是其他的对象呢? 例如 UserApi 和 UserService 呢?我们并没有向 graph 调用 Provide 过。那么它们是怎么完成赋值和注入的呢?

其实从下面这张对象依赖图能够很简单的看清楚。

在这里插入图片描述
从这个依赖图中可以看出,conf 和 db 对象是属于根节点,所有的对象都依赖和包含着它们。而 server 属于叶子节点,不会有其他对象依赖它了。

我们需要提供给 Graph 的就是根节点和叶子节点,而对于中间节点来说,完全可以通过根节点和叶子节点推导出来。Graph 会通过 inject:“” 标签,自动将中间节点 Provide 到 Graph 中,进行注入。

对以上例子,我们深入剖析下 Graph 内部进行 Populate 时都发生了哪些动作:

Graph 首先解析 server 对象,发现其有两个标记为 inject 的字段:UserApi 和 PostApi。其类型 UserController 和 PostController, Graph 中从未出现过这两个类型。因此,Graph 会自动对该字段调用 Provide,提供给 Graph。
解析 UserApi 时,发现其依然有也有两个标记为 inject 的字段:UserService 和 Conf。对于 UserService 这种 Graph 中未登记过的类型,会自动 Provide。而对 Conf, Graph 中之前已经注册过了,因此直接将注册的对象赋值给该字段即可。
接下来就是继续逐步解析,直至没有tag为 inject 的字段。
以上就是整个依赖注入的流程了。

这里需要注意的是,在我们上面的示例中,以这种方式注入,其中所有的对象都相当于单例对象。即一个类型,只会在 Graph 中存在一个实例对象。比如 UserController 和 PosterController 中的 UserService 实际上是同一个对象。

我们的 main 函数使用 inject 进行改造后,将会变得非常简洁。而且即使随着业务越来越复杂,Handler 和 Service 越来越多,这个 main 函数中的注入逻辑也不会任何改变,除非有新的根节点对象出现。

当然,对于 Graph 来说,也不是只能 Provide 根节点和叶子节点,我们也可以自行 Provide 一个 UserService 的实例进去,对于 Graph 的运作是没有任何影响的。只不过只 Provide 根节点和叶子节点,代码会看起来更简洁一些。

inject 的高级用法
我们在声明 tag 时,除了声明为 inject:“” 这种默认用法外,还可以有其他三种高级的用法:

inject:“private”。私有注入。
inject:“inline”。内联注入。
inject:“object_name”。命名注入,这里的 object_name 可以取成任意的名字。
private (私有注入)
我们上文讲过,默认情况下,所有的对象都是单例对象。一个类型只会有一个实例对象存在。但也可以不使用单例对象,private 就是提供了这种可能。

例如:

type UserController struct {
    
    
	UserService *UserService `inject:"private"`
	Conf        *Conf        `inject:""`
}

将 UserController 中的 UserService 属性声明为 private 注入。这样的话,graph 遇到 private 标签时,会自动的 new 一个全新的 UserService 对象,将其赋值给该字段。

这样 Graph 中就同时存在了两个 UserService 的实例,一个是 UserService 的全局实例,给默认的 inject:“” 使用。一个是专门给 UserController 实例中的 UserService 使用。

但在实际开发中,这种 private 的场景似乎也比较少,大部分情况下,默认的单例对象就足够了。

inline (内联注入)
默认情况下,需要注入的属性必须得是 *Struct。但是也是可以声明为普通对象的。例如:

将 UserController 中的 UserService 属性声明为 private 注入。这样的话,graph 遇到 private 标签时,会自动的 new 一个全新的 UserService 对象,将其赋值给该字段。

这样 Graph 中就同时存在了两个 UserService 的实例,一个是 UserService 的全局实例,给默认的 inject:"" 使用。一个是专门给 UserController 实例中的 UserService 使用。

但在实际开发中,这种 private 的场景似乎也比较少,大部分情况下,默认的单例对象就足够了。

inline (内联注入)
默认情况下,需要注入的属性必须得是 *Struct。但是也是可以声明为普通对象的。例如:

type UserController struct {
    
    
	UserServ

注意,这里的 UserService 的类型,并非是 *UserService 指针类型了,而是普通的 struct 类型。struct 类型在 Go 里面都是值语义,这里当然也就不存在单例的问题了。

命名注入
如果我们需要对某些字段注入专有的对象实例,那么我们可能会用到命名注入。使用方法就是在 inject 的 tag 里写上专有的名字。如下:

type UserController struct {
    
    
	UserService UserService `inject:"named_service"`
	Conf        *Conf       `inject:""`
}

当然,这个命名肯定不能命名为 private 和 inline,这两个属于inject的保留词。

同时,我们一定要把这个命名实例 Provide 到 graph 里面,这样 graph 才能把两个对象联系起来。

graph.Provide(
	&inject.Object{
    
    
		Value: &namedService,
		Name: "named_service",
	},
);

除了可以注入对象外,还可以注入 map。如下:

type UserController struct {
    
    
	UserService UserService       `inject:"inline"`
	Conf        *Conf             `inject:""`
	UserMap     map[string]string `inject:"private"`
}

需要注意的是,map 的注入 tag 一定要是 inject:“private”。

facebookgo/inject 的缺陷
facebookgo/inject 固然很好用,只要声明 inject:“” 的 tag,提供几个对象,就可以完全自动的注入所有依赖关系。

但是由于Golang本身的语言设计, facebookgo/inject 也会有一些缺陷和短板:

所有需要注入的字段都需要是 public 的。 这也是 Golang 的限制,不能对私有属性进行赋值。所以只能对public的字段进行注入。但这样就会把代码稍显的不那么优雅,毕竟很多变量我们其实并不想 public。

只能进行属性赋值,不能执行初始化函数。 facebookgo/inject只会帮你注入好对象,把各个属性赋值好。但很多时候,我们往往需要在对象赋值完成后,再进行其他一些动作。但对于这个需求场景,facebookgo/inject并不能很好的支持。

这两个问题的原因总结归纳为:Golang没有构造函数。


  1. 依赖注入和控制反转
    正常情况下,对函数或方法的调用是调用方的主动直接行为,调用方清楚地知道被调的函数名是什么,参数有哪些类型,直接主动地调用;包括对象的初始化也是显式地直接初始化。所谓的“控制反转”就是将这种主动行为变为间接的行为,主调方不是直接调用函数或对象,而是借助框架代码进行间接的调用和初始化,这种行为被称为“控制反转”,控制反转可以解耦调用方和被调方。

一般情况下,使用库的程序是程序主动地调用库的功能,但使用框架的程序常常由框架驱动整个程序,在框架下写的业务代码是被框架驱动的,这种模式就是“控制反转”。
“依赖注入”是实现“控制反转”的一种方法,是通过注入的参数或实例的方式实现控制反转。
控制反转的价值在哪里?一句话“解耦”,可以让控制反转的框架代码读取配置,动态地构建对象。
控制反转是解决复杂问题的一种方法,特别是在web框架中为路由和中间件的灵活注入提供了很好的方法。
2. inject
inject是Go语言依赖注入的实现,它实现了对结构(struct)和函数的依赖注入。首先思考如何通过一个字符串类型的函数名调用函数。能想到的方法是使用map实现一个字符串到函数的映射。代码如下:

package main

func f1() {
    
    
	println("f1")
}
func f2() {
    
    
	println("f2")
}

func main() {
    
    
	funcs := make(map[string]func())
	funcs["f1"] = f1
	funcs["f2"] = f2

	funcs["f1"]()
	funcs["f2"]()
}

首先第一个问题是map的Value类型被写成了func(),这个不能适用于不同参数和返回值类型的函数。可以解决的方法是将map的Value定义为interface{}空接口类型,但是需要借助类型断言或反射来实现。inject包借助反射实现函数的注入调用。代码如下:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type S1 interface{
    
    }
type S2 interface{
    
    }

func Format(name string, company S1, level S2, age int) {
    
    
	fmt.Printf("name=%s, company=%s, level=%s, age=%d!\n", name, company, level, age)
}

func main() {
    
    
	//控制实例的创建
	inj := inject.New()
	//实参注入
	inj.Map("Tom")
	inj.MapTo("tencent", (*S1)(nil))
	inj.MapTo("T4", (*S2)(nil))
	inj.Map(23)

	//函数反转调用
	inj.Invoke(Format)

}

inject提供了一种注入参数调用函数的通用功能,inject.New()相当于创建了一个控制实例,由其来实现对函数的注入调用。inject包不但提供了对函数的注入,还实现了对struct类型的注入。例如:

package main

import (
	"fmt"
	"github.com/codegangsta/inject"
)

type S1 interface{
    
    }
type S2 interface{
    
    }
type Staff struct {
    
    
	Name    string `inject`
	Company S1     `inject`
	Level   S2     `inject`
	Age     int    `inject`
}

func main() {
    
    
	//创建被注入实例
	s := Staff{
    
    }
	//控制实例的创建
	inj := inject.New()
	//初始化注入值
	inj.Map("tom")
	inj.MapTo("tencent", (*S1)(nil))
	inj.MapTo("T4", (*S2)(nil))
	inj.Map(23)
	//实现对 struct 注入
	inj.Apply(&s)
	//打印结果
	fmt.Printf("s = %v\n", s)
}

  1. inject实现原理分析
    inject包只有 2 个文件,一个是 inject.go 文件和一个 inject_test.go 文件。其中inject.go 短小精悍,包括注释和空行在内才 157 行代码,却提供了一个完美的依赖注入实现。
    入口函数New
    inject.New()函数构建一个具体类型injector实例作为内部注入引擎,返回的是一个Injector类型的接口。这里体现了一种面向接口的设计思想:对外暴漏接口方法,对内隐藏内部实现。
// New returns a new Injector.
func New() Injector {
    
    
	return &injector{
    
    
		values: make(map[reflect.Type]reflect.Value),
	}
}

New 方法用于初始化 injector struct,并返回一个指向 injector struct 的指针,但是这个返回值被 Injector 接口包装了。
接口设计
inject.go代码中定义了 4 个接口,包括一个父接口和三个子接口,如下所示:

type Injector interface {
    
    
    Applicator //抽象生成注入结构实例的接口
    Invoker //抽象函数调用的接口
    TypeMapper //抽象注入参数的接口
    SetParent(Injector) //实现一个注入实例链,下游的能覆盖上游的类型
}

type Applicator interface {
    
    
    Apply(interface{
    
    }) error //Appky方法实现对结构的注入
}

type Invoker interface {
    
    
    Invoke(interface{
    
    }) ([]reflect.Value, error) //Invoke方法是对被注入实参函数的调用
}

type TypeMapper interface {
    
    
    // 基于调用reflect.TypeOf得到的类型映射interface{
    
    }的值。
    Map(interface{
    
    }) TypeMapper
    // 基于提供的接口的指针映射interface{
    
    }的值。
    // 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。
    MapTo(interface{
    
    }, interface{
    
    }) TypeMapper
    // 为直接插入基于类型和值的map提供一种可能性。
    // 它使得这一类直接映射成为可能:无法通过反射直接实例化的类型参数,如单向管道。
    Set(reflect.Type, reflect.Value) TypeMapper
    // 返回映射到当前类型的Value. 如果Type没被映射,将返回对应的零值。
    Get(reflect.Type) reflect.Value
}

Injector 接口是 Applicator、Invoker、TypeMapper 接口的父接口,所以实现了 Injector 接口的类型,也必然实现了 Applicator、Invoker 和 TypeMapper 接口:

Applicator 接口只规定了 Apply 成员,它用于注入 struct。
Invoker 接口只规定了 Invoke 成员,它用于执行被调用者。
TypeMapper 接口规定了三个成员,Map 和 MapTo 都用于注入参数,但它们有不同的用法,Get 用于调用时获取被注入的参数。
另外 Injector 还规定了 SetParent 行为,它用于设置父 Injector,其实它相当于查找继承。也即通过 Get 方法在获取被注入参数时会一直追溯到 parent,这是个递归过程,直到查找到参数或为 nil 终止。

Injector暴漏了所有方法给外部使用者,这些方法又可以归纳为两大类:

第一类方法是对参数注入进行初始化,将结构类型的字段的注入和函数的参数注入统一成一套方法实现;
第二类是专用注入实现,分别是生成结构对象和调用函数方法。
注意:无论函数的实参,还是结构的字段,在inject内部,都存放在map[reflecct.Type] reflect.Value类型的map里面。

整个inject包的处理流程如下:

通过inject.New()创建注入引擎,注入引擎被隐藏,返回的是Injector接口类型变量
调用TypeMapper接口(Injector内嵌TypeMapper)的方法注入struct的字段值或函数的实参值
调用Invoker方法执行被注入的函数,或者调用Applicator接口方法获得被注入后的结构实例。

内部实现:
首先查看injector的数据结构。injector 是 inject 包中唯一定义的 struct,所有的操作都是基于 injector struct 来进行的,它有两个成员 values 和 parent。values 用于保存注入的参数,是一个用 reflect.Type 当键、reflect.Value 为值的 map,理解这点将有助于理解 Map 和 MapTo。

type injector struct {
    
    
	values map[reflect.Type]reflect.Value
	parent Injector
}

// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not an pointer to an interface.
func InterfaceOf(value interface{
    
    }) reflect.Type {
    
    
	t := reflect.TypeOf(value)

	for t.Kind() == reflect.Ptr {
    
    
		t = t.Elem()
	}

	if t.Kind() != reflect.Interface {
    
    
		panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
	}

	return t
}

values里面存放的可以是被注入struct的字段类型和值,也可以是函数实参的类型和值。注意values是以reflect.Type为Key的map,如果一个结构的字段类型相同,则后面注入的参数会覆盖前面的参数,规避的方法是使用MapTo方法,通过抽象出一个接口类型来避免覆盖。

InterfaceOf 方法虽然只有几句实现代码,但它是 Injector 的核心。InterfaceOf 方法的参数必须是一个接口类型的指针,如果不是则引发 panic。InterfaceOf 方法的返回类型是 reflect.Type,注意: injector 的成员 values 就是一个 reflect.Type 类型当键的 map。这个方法的作用其实只是获取参数的类型,而不关心它的值。InterfaceOf 方法就是用来得到参数类型,而不关心它具体存储的是什么值。

func (i *injector) MapTo(val interface{
    
    }, ifacePtr interface{
    
    }) TypeMapper {
    
    
	i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
	return i
}

injector里面的parent的作用是实现多个注入引擎,其构成了一个链。

injector对函数的注入实现如下:

// Invoke attempts to call the interface{
    
    } provided as a function,
// providing dependencies for function arguments based on Type.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
// It panics if f is not a function
func (inj *injector) Invoke(f interface{
    
    }) ([]reflect.Value, error) {
    
    
	t := reflect.TypeOf(f) //获取函数类型的Type

	// 构造一个存放函数实参Value值的数组
	var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
	//使用反射获取函数实参reflect.Type,逐个去injector中查找注入的Value值
	for i := 0; i < t.NumIn(); i++ {
    
    
		argType := t.In(i)
		val := inj.Get(argType)
		if !val.IsValid() {
    
    
			return nil, fmt.Errorf("Value not found for type %v", argType)
		}

		in[i] = val
	}

	//反射调用函数
	return reflect.ValueOf(f).Call(in), nil
}

Invoke 方法用于动态执行函数,当然执行前可以通过 Map 或 MapTo 来注入参数,因为通过 Invoke 执行的函数会取出已注入的参数,然后通过 reflect 包中的 Call 方法来调用。Invoke 接收的参数 f 是一个接口类型,但是 f 的底层类型必须为 func,否则会 panic。

**Apply 方法是用于对 struct 的字段进行注入,参数为指向底层类型为结构体的指针。**可注入的前提是:字段必须是导出的(也即字段名以大写字母开头),并且此字段的 tag 设置为inject。

// Maps dependencies in the Type map to each field in the struct
// that is tagged with 'inject'.
// Returns an error if the injection fails.
func (inj *injector) Apply(val interface{
    
    }) error {
    
    
	v := reflect.ValueOf(val)

	for v.Kind() == reflect.Ptr {
    
    
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
    
    
		return nil // Should not panic here ?
	}

	t := v.Type()

	for i := 0; i < v.NumField(); i++ {
    
    
		f := v.Field(i)
		structField := t.Field(i)
		if f.CanSet() && (structField.Tag == "inject" || structField.Tag.Get("inject") != "") {
    
    
			ft := f.Type()
			v := inj.Get(ft)
			if !v.IsValid() {
    
    
				return fmt.Errorf("Value not found for type %v", ft)
			}

			f.Set(v)
		}

	}

	return nil
}

Map和MapTo方法比较
**Map 和 MapTo 方法都用于注入参数,保存于 injector 的成员 values 中。**这两个方法的功能完全相同,唯一的区别就是 Map 方法用参数值本身的类型当键,而 MapTo 方法有一个额外的参数可以指定特定的类型当键。但是 MapTo 方法的第二个参数 ifacePtr 必须是接口指针类型,因为最终 ifacePtr 会作为 InterfaceOf 方法的参数。

// Maps the concrete value of val to its dynamic type using reflect.TypeOf,
// It returns the TypeMapper registered in.
func (i *injector) Map(val interface{
    
    }) TypeMapper {
    
    
	i.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
	return i
}

func (i *injector) MapTo(val interface{
    
    }, ifacePtr interface{
    
    }) TypeMapper {
    
    
	i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
	return i
}

// Maps the given reflect.Type to the given reflect.Value and returns
// the Typemapper the mapping has been registered in.
func (i *injector) Set(typ reflect.Type, val reflect.Value) TypeMapper {
    
    
	i.values[typ] = val
	return i
}

func (i *injector) Get(t reflect.Type) reflect.Value {
    
    
	val := i.values[t]

	if val.IsValid() {
    
    
		return val
	}

	// no concrete types found, try to find implementors
	// if t is an interface
	if t.Kind() == reflect.Interface {
    
    
		for k, v := range i.values {
    
    
			if k.Implements(t) {
    
    
				val = v
				break
			}
		}
	}

	// Still no type found, try to look it up on the parent
	if !val.IsValid() && i.parent != nil {
    
    
		val = i.parent.Get(t)
	}

	return val

}

func (i *injector) SetParent(parent Injector) {
    
    
	i.parent = parent
}

为什么需要有 MapTo 方法?因为注入的参数是存储在一个以类型为键的 map 中,可想而知,当一个函数中有一个以上的参数的类型是一样时,后执行 Map 进行注入的参数将会覆盖前一个通过 Map 注入的参数。

SetParent 方法用于给某个 Injector 指定父 Injector。Get 方法通过 reflect.Type 从 injector 的 values 成员中取出对应的值,它可能会检查是否设置了 parent,直到找到或返回无效的值,最后 Get 方法的返回值会经过 IsValid 方法的校验。

inject对函数注入调用实现很间接,就是从injector里面获取函数实参,然后调用函数。

示例代码如下:

package main

import (
    "fmt"
    "github.com/codegangsta/inject"
)

type SpecialString interface{
    
    }

type TestStruct struct {
    
    
    Name   string `inject`
    Nick   []byte
    Gender SpecialString `inject`
    uid    int           `inject`
    Age    int           `inject`
}

func main() {
    
    
    s := TestStruct{
    
    }
    inj := inject.New()
    inj.Map("张三")
    inj.MapTo("男", (*SpecialString)(nil))
    inj2 := inject.New()
    inj2.Map(26)
    inj.SetParent(inj2)
    inj.Apply(&s)
    fmt.Println("s.Name =", s.Name)
    fmt.Println("s.Gender =", s.Gender)
    fmt.Println("s.Age =", s.Age)
}

  1. 反射的优缺点
    优点:通用性、灵活性
    缺点:反射是脆弱的、反射是晦涩难懂的、反射有部分性能损失(提供了动态修改程序状态的能力,必然不是直接的地址引用,而是要借助运行时构造一个抽象层)
    反射的最佳实践:1. 在库或框架内部使用反射,而不是把反射结构暴漏给调用者;2. 框架代码才考虑使用反射,一般业务没有必要抽象到参设的层次;3. 除非没有其他办法,否则不要使用反射技术。

猜你喜欢

转载自blog.csdn.net/u014374009/article/details/129003378