Golang 手撸 IoC 容器
0x00 什么是 IoC
IoC (控制反转)并不是一个新鲜的概念,如果之前有过 Java 经验,那么肯定不会对 IoC 陌生,而 PHP 近几年的框架其实也开始出现了大量的容器应用,在这里需要注意 IoC 和 IoC 容器是两个概念,不要混淆。
IoC 是编程思想,而 IoC 容器是帮助我们进行 IoC 的工具,我们通过控制反转把对象之间的调用过程交给 IOC 容器来完成,而最终实现业务代码的解耦。
在 Golang 中 IOC 容器的使用时候,我们要根据我们的具体情况还进行分析是否要使用 IOC :
- 如果你的项目对于性能要求比较高,最好就不要使用 IOC 容器了,因为在 Golang 中实现 IoC 要使用到反射,而反射使用的
堆
空间,如果大量使用反射就会对 Golang 的 GC 造成一定的压力。 - 要是你的项目对于性能要求不高,追求的是业务的封装性和可维护性,那么就可以考虑使用 IoC 容器。
如果你不了解什么是 IoC 也没有关系,下面会带大家简单的了解一下什么是 IOC 容器。
下面来简单解释一下什么是 IoC。
假设现在有两个 Service:UserService 和 OrderService。
- UserService: 用户服务, GetUserInfo 获取用户信息
- OrderService: 订单服务, GetOrderInfo 获取订单信息。
这两个 Service 中的方法传递的参数也都是用户 id,现在以代码的形式来模拟一下这个场景。
初始目录如下:
.
├── go.mod
├── main.go
└── svc #service 目录
├── OrderService.go
└── UserService.go
UserService代码如下:
package svc
import "fmt"
type UserService struct {
}
func NewUserService() *UserService {
return &UserService{}
}
func (this *UserService) GetUserInfo(uid int) {
fmt.Println("获取用户 id = ", uid, "的详细信息")
}
OrderService
package svc
import "fmt"
type OrderService struct {
}
func NewOrderService() *OrderService {
return &OrderService{}
}
func (this *OrderService) GetOrderInfo(uid int) {
fmt.Println("获取用户 id = ", uid, "的订单信息")
}
基础的 Service 已经封装好了,接下来进行调用。
package main
import "study-go/svc"
func main() {
uid := 123
orderSvc := svc.NewOrderService()
userSvc := svc.NewUserService()
//获取用户详细
userSvc.GetUserInfo(uid)
orderSvc.GetOrderInfo(uid)
}
如果代码写到这个程度是否就可以结束了,答案是可以的,并不是很 Low,在 Golang 中并不是要写成和 Java 一样,有大量的设计模式和类与类之间的嵌套。
如果这个时候时候你的领导跟你说要将获取订单信息的方法放到 UserService 里面,你应该怎么做?
package svc
import "fmt"
type UserService struct {
}
func NewUserService() *UserService {
return &UserService{}
}
func (this *UserService) GetUserInfo(uid int) {
fmt.Println("获取用户 id = ", uid, "的详细信息")
}
func (this *UserService) GetOrderInfo(uid int) {
NewOrderService().GetOrderInfo(uid)
}
你完全可以这样写,因为获取用户订单信息是和用户相关的,没有必要放在两个 Service 里面,这个时候我们可以视作 UserService 依赖于 OrderService ,而我们处理的方式是主动处理依赖(在方法内初始化了 NewOrderService),也就是将依赖硬编码到我们的代码中,也叫做控制正转。
调用的时候就变成了这个样子:
package main
import "study-go/svc"
func main() {
uid := 123
userSvc := svc.NewUserService()
//获取用户详细
userSvc.GetUserInfo(uid)
userSvc.GetOrderInfo(uid)
}
而我们要讲的是依赖反转是被动的,也就是说在我们的 Service 代码里面不会有任何主动初始化 OrderService 的代码,我们需要将我们代码更改一下:
package svc
import "fmt"
type UserService struct {
orderSvc *OrderService
}
func NewUserService(order *OrderService) *UserService {
return &UserService{orderSvc: order}
}
func (this *UserService) GetUserInfo(uid int) {
fmt.Println("获取用户 id = ", uid, "的详细信息")
}
func (this *UserService) GetOrderInfo(uid int) {
this.orderSvc.GetOrderInfo(uid)
}
观察上方代码的变化:
- 我们首先在 UserService 结构体定时以的时候定义了一个新的属性 orderSvc;
- 改造了构造方法,在初始化 UserService 的时候,需要给 UserService 传递一个 OrderService 进来;
- 在 GetOrderInfo 方法中,因为在构造 UserService 的时候已经传递来了一个 OrderService ,所以可以通过属性的方法进行调用 orderSvc.GetOrderInfo;
那么在调用的时候就变成了这个样子:
package main
import "study-go/svc"
func main() {
uid := 123
userSvc := svc.NewUserService(svc.NewOrderService())
//获取用户详细
userSvc.GetUserInfo(uid)
userSvc.GetOrderInfo(uid)
}
这个时候,UserService 就被动的 接收了依赖,这就叫做控制反转,也就是 IOC。
这个时候其实是有一些问题的,如果 UserService 有十七八个依赖,我们要怎么样进行处理?这个时候就需要一个工具来帮助我们统一进行管理那就是 IOC 容器,这一部分内容将在下一小节进行讲解。
0x01 利用 Map 存储 Service
在上一小节讲过 IoC 容器是用来帮助我们处理 IoC 的工具,所以我们需要保存两个 Service 之间的依赖关系。
在编写容器代码前,我们现将 OrderService 改造一下,在其中加上 version。
type OrderService struct {
version string
}
func NewOrderService() *OrderService {
return &OrderService{version: "1"}
}
我们通过 version 来判断后面我们放到容器中的 OrderService 和取出来的是否一致。
我们先来实现一个基础版本的容器,使用 Map 来存储依赖关系:
package injector
import "reflect"
type BeanMapper map[reflect.Type]reflect.Value
func (this BeanMapper) add(bean interface{}) {
t := reflect.TypeOf(bean)
if t.Kind() != reflect.Ptr {
panic("require ptr object")
}
this[t] = reflect.ValueOf(bean)
}
在 Map 中 key 是 reflect.Type, 而 value 是 reflect.Value。
我们来看 add 方法,其实就是将依赖关系存储到 map 中,在使用的时候再从 map 中取出来,在这里为了简化操作,我们目前值允许指针被加入到 map 中。
package injector
var BeanFactory *BeanFactoryImpl
func init() {
BeanFactory = NewBeanFactory()
}
type BeanFactoryImpl struct {
BeanMapper BeanMapper
}
func (this *BeanFactoryImpl) Set(vList ...interface{}) {
if vList == nil || len(vList) == 0 {
return
}
for _, v := range vList {
this.BeanMapper.add(v)
}
}
func NewBeanFactory() *BeanFactoryImpl {
return &BeanFactoryImpl{BeanMapper: make(BeanMapper)}
}
现在创建和添加已经写好了,在这里,我们使用工厂模式来创建容器,init
函数是初始化函数,他在 main
函数执行前执行,用来初始化资源或者提前创建对象。
现在我们再给容器添加获取代码,首先给 mapper 添加 get 方法,很简单,其实就是依靠 map 的获取操作。
func (this BeanMapper) get(bean interface{}) reflect.Value {
t := reflect.TypeOf(bean)
if v, ok := this[t]; ok {
return v
}
return reflect.Value{}
}
然后再添加 Factory 的获取方法。
func (this *BeanFactoryImpl) Get(v interface{}) interface{} {
if v == nil {
return nil
}
bean := this.BeanMapper.get(v)
if bean.IsValid() {
return bean.Interface()
}
return nil
}
这样一来我们就完成了容器的添加依赖和获取依赖,我们来实践一下。
package main
import (
"fmt"
"study-go/injector"
"study-go/svc"
)
func main() {
injector.BeanFactory.Set(svc.NewOrderService())
orderSvc := injector.BeanFactory.Get((*svc.OrderService)(nil))
fmt.Println(orderSvc)
}
执行一下:
&{2}
输出的时候将 OrderService 中的 version 给打印了出来,这证明我们可以从容器中取到放进去的值。
0x02 处理依赖注入
我们的 UserService 依赖了 OrderService,我们之前使用手动编码来处理依赖关系,现在我们有了容器,现在我们来使用容器来自动注入。
做法其实有很多方案,在Java 可以使用注解, PHP 中使用注释来模拟注解,而在 Golang 中的处理方式稍有不同,因为在 Golang 中并没有对注解的原生支持,所以我们需要使用 Tag 来实现,Tag 是用于标识结构体字段的额外信息,标准库 reflect
包提供了操作 Tag 的方法[1]。
package svc
import "fmt"
type UserService struct {
OrderSvc *OrderService `inject:"-"`
}
func NewUserService(order *OrderService) *UserService {
return &UserService{OrderSvc: order}
}
func (this *UserService) GetUserInfo(uid int) {
fmt.Println("获取用户 id = ", uid, "的详细信息")
}
func (this *UserService) GetOrderInfo(uid int) {
this.OrderSvc.GetOrderInfo(uid)
}
首先我们将 Uservice 中的 orderSvc 改为公开的属性,然后在其后面增加 inject
tag,其中 "-" 其实写不写都无所谓,但是因为我们在业务开发的时候,可能还会有 json tag ,所以我们在这里约定 inject:"-"
就等于我要使用依赖注入。
接下来,我们在 BeanFactory 中实现 Apply 方法,用来处理依赖注入。
func (this *BeanFactoryImpl) Apply(bean interface{}) {
if bean == nil {
return
}
v := reflect.ValueOf(bean)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return
}
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {
if getV := this.Get(field.Type); getV != nil {
v.Field(i).Set(reflect.ValueOf(getV))
}
}
}
}
首先在代码中判断了是否为结构体,并且从 map 中获取已经实例化好的 Service,在这里我们还需要改造一下 Map 的 get 方法,因为这个时候传递的 field.Type
在 get 方法中会报错,我们需要兼容一下这个类型,在原有代码中增加一下断言。
func (this BeanMapper) get(bean interface{}) reflect.Value {
var t reflect.Type
//断言一下 bean
if bt, ok := bean.(reflect.Type); ok {
t = bt
} else {
t = reflect.TypeOf(bean)
}
if v, ok := this[t]; ok {
return v
}
return reflect.Value{}
}
现在让我们来试一下新的成果。
func main() {
injector.BeanFactory.Set(svc.NewOrderService())
userService := svc.NewUserService()
injector.BeanFactory.Apply(userService)
fmt.Println(userService.OrderSvc)
}
现在我们的依赖管理采用 Apply 方法进行管理,Apply 方法自动在容器中寻找其依赖项,然后注入到 UserService 中,运行程序显示结果如下:
&{2}
0x03 动态注入 & Tag 支持表达式
在这个阶段,我们要是实现一个能够基于 Tag 表达式能够动态注入的功能,因为如果项目不复杂,只有几个 Service,那么我们还可以进行手工的将 Service 添加到容器中,那么如果现在随着业务的增加,Service 的数量增加到 20 个,甚至 100 个,我们就需要在 BeanFactory.Set() 方法的参数中写大量的代码来完成容器的初始化。
这么做很显然是不够好的,以为一个完整的容器是需要能够动态注入的,我们只需要基于简单的配置,就可以完成动态注入。
首先我们先创建一个新的文件 ServiceConfig.go 我们将在 ServiceConfig Struct 中来管理我们依赖的创建。
package config
import "study-go/svc"
type ServiceConfig struct {
}
func NewServiceConfig() *ServiceConfig {
return &ServiceConfig{}
}
func (this *ServiceConfig) OrderService() *svc.OrderService {
return svc.NewOrderService()
}
以后业务中所有依赖实例化我们都存放在这个文件中,这种做法其实是类似于 Java Spring 的做法。
上面刚才说了,我们要实现一个能够基于 Tag 动态注入的功能,目前字段 Tag 中给的约定是 "-"
,用来直接去到容器中被实例化好的 Service,我们可以更近一步,将约定在增加一份,也就是所谓的表达式,例如 inject:"ServiceConfig.OrderService()
。
根据这个表达式可以清晰的看出,该字段依赖于 ServiceConfig.OrderService,那么只需要 BeanFactory 代替我们去把这个值拿到就可以了。
但是目前 BeanFactory 目前只有一个 BeanMap 来存储被实例化好的 struct ,并不支持表达式,所以我们需要先增加一个字段。
type BeanFactoryImpl struct {
BeanMapper BeanMapper // 存储依赖
ExprMap map[string]interface{} // 新增的字段,利用表达式的方式存储依赖
}
新增的 ExprMap 中 key 存储的就是我们需要支持的表达式,而他的 value 其实就是我们的实例化好的 struct。
接下来,我们先引入一个新的库 github.com/shenyisyn/goft-expr
,这个库的作用是使用表达式来执行函数或者 struct,我们来修改 BeanFactory 中 的 Apply 方法,以支持新的表达式。
func (this *BeanFactoryImpl) Apply(bean interface{}) {
if bean == nil {
return
}
v := reflect.ValueOf(bean)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return
}
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {
// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
if getV := this.Get(field.Type); getV != nil {
v.Field(i).Set(reflect.ValueOf(getV))
}
} else {
fmt.Println("使用了表达式")
ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
if ret != nil && !ret.IsEmpty() {
v.Field(i).Set(reflect.ValueOf(ret[0]))
}
}
}
}
}
我们来看这一段代码:
// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
...
} else {
ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
if ret != nil && !ret.IsEmpty() {
v.Field(i).Set(reflect.ValueOf(ret[0]))
}
首先是判断了 inject 是否等于 - 符号,这样是为了兼容老的表达式,也就是默认去读取依赖的 Service,然后在下方,我们去根据表达式取出了已经在容器中准备好的 Service。
这一切都已经准备好了,我们来先试验一下根据表达式来进行注入,先修改 UserService:
type UserService struct {
OrderSvc *OrderService `inject:"ServiceConfig.OrderService()"`
}
然后是 main.go
func main() {
srvConfig := config.NewServiceConfig()
injector.BeanFactory.ExprMap = map[string]interface{}{
"ServiceConfig": srvConfig,
}
injector.BeanFactory.Set(srvConfig)
userService := svc.NewUserService()
injector.BeanFactory.Apply(userService)
fmt.Println(userService.OrderSvc)
}
接下来执行结果如下:
➜ study-go go run main.go
&{2}
接下来,我们继续优化我们的代码,能够让代码真正的动态注入,在上面,我们需要手动配置 ExprMap ,但是其实我们同样可以使用反射,来自动完成这一配置,这一次我们先来看 main.go
func main() {
srvConfig := config.NewServiceConfig()
injector.BeanFactory.Set(srvConfig)
userService := svc.NewUserService()
injector.BeanFactory.Apply(userService)
fmt.Println(userService.OrderSvc)
}
BeanFactory 新增 Config 方法:
func (this *BeanFactoryImpl) Config(cfgs ...interface{}) {
for _, cfg := range cfgs {
t := reflect.TypeOf(cfg)
if t.Kind() != reflect.Pointer {
panic("required ptr object!")
}
this.Set(cfg) //把 cfg 本身也加入到 bean 中
t = t.Elem()
this.ExprMap[t.Name()] = cfg // 自动设置 ExprMap
v := reflect.ValueOf(cfg).Elem()
for i := 0; i < t.NumMethod(); i++ {
method := v.Method(i)
callRet := method.Call(nil)
if callRet != nil && len(callRet) == 1 {
this.Set(callRet[0].Interface())
}
}
}
}
这段代码其实很好理解:
- 判断是否为指针,因为这里我们需要的是被实例化好的 Strcut
- 然后将 ServiceConfig 也放入到 BeanMap 当中
- 然后设置 ExprMap
- 最后序列化 ServiceConfig 中的所有 NewXXX 函数
这样一来动态注入就已经完成了
0x04 支持接口注入
如果要让我们的容器支持接口,其实也非常简单,只需要修改 BeanMap 中的 Get 方法即可:
func (this BeanMapper) get(bean interface{}) reflect.Value {
...
if v, ok := this[t]; ok {
return v
}
//如果没有找到那就去看看接口
for k, v := range this {
if k.Implements(t) {
return v
}
}
return reflect.Value{}
}
0x05 处理循环依赖
当我们的 Service 存在多级嵌套的时候,按照现在的代码逻辑是无法去自动完成下一级的自动注入的,我们仅需要在apply 完成的时候,在对其依赖进行递归调用就可以
// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
if getV := this.Get(field.Type); getV != nil {
v.Field(i).Set(reflect.ValueOf(getV))
this.Apply(getV)
}
} else {
ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
if ret != nil && !ret.IsEmpty() {
retV := ret[0]
v.Field(i).Set(reflect.ValueOf(retV))
this.Apply(retV)
}
}
0x06 IOC 容器还需要做的事情
到目前为止,我们的 基础版的 IOC 就已经完成了,但是目前还有两个问题没有解决:
- 单例和多例,目前这个 IOC 容器,每一次在执行的时候其实都是会调用 NewXXX 实例化出来一个依赖,所以需要能够支持单例或者多例
- ServiceConfig 目前还不支持参数实例化
后面如果有时间的话,我会将这块补上。
0x07 结尾
在这里首先要感谢程序员在囧途的沈逸的课程,这篇文章其实就是以 Golang 手撸 IOC 容器为基础编写的,大部分都是他讲课的内容,我在其基础上进行了一些编辑,并且引用了其他的一些文章内容来充实整篇文章,目前与课程不同的是没有去实现单例和多例。