Container - 为 Go语言而生的运行时依赖注入容器

Container 是一款为 Go 语言开发的运行时依赖注入库。Go 语言的语言特性决定了实现一款类型安全的依赖注入容器并不太容易,因此 Container 大量使用了 Go 的反射机制。如果你的使用场景对性能要求并不是那个苛刻,那 Container 非常适合你。

并不是说对性能要求苛刻的环境中就不能使用了,你可以把 Container 作为一个对象依赖管理工具,在你的业务初始化时获取依赖的对象。

使用方式

go get github.com/mylxsw/container

要创建一个 Container 实例,使用 containier.New 方法

cc := container.New()

此时就创建了一个空的容器。

你也可以使用 container.NewWithContext(ctx) 来创建容器,创建之后,可以自动的把已经存在的 context.Context 对象添加到容器中,由容器托管。

对象绑定

在使用之前,我们需要先将我们要托管的对象告诉容器。Container 支持三种类型的对象管理

  • 单例对象 Singleton
  • 原型对象(多例对象) Prototype
  • 字符串值对象绑定 Value

所有的对象绑定方法都会返回一个 error 返回值来说明是否绑定成功,应用在使用时一定要主动去检查这个 error

确定对象一定会绑定成功(一般不违反文档中描述的参数签名方式,都是一定会成功的)或者要求对象必须要绑定成功(通常我们都要求这样,不然怎么进行依赖管理呢),则可以使用 Must 系列方法,比如 Singleton 方法对应的时 MustSingleton,当创建出错时,该方法会直接 panic

绑定对象时,SingletonPrototypeBindValue 方法对于同一类型,只能绑定一次,如果多次绑定同一类型对象的创建函数,会返回 ErrRepeatedBind 错误。

有时候,希望对象创建函数可以多次重新绑定,这样就可以个应用更多的扩展性,可以随时替换掉对象的创建方法,比如测试时 Mock 对象的注入。这时候我们可以使用 Override 系列方法:

  • SingletonOverride
  • PrototypeOverride
  • BindValueOverride

使用 Override 系列方法时,必须保证第一次绑定时使用的是 Override 系列方法,否则无法重新绑定。

也就是说,可以这样绑定 SingletonOverride -> SingletonOverrideSingletonOverride -> Singleton,但是一旦出现 Singleton,后续就无法对该对象重新绑定了。

单例对象

使用 Singleton 系列的方法来将单例对象托管给容器,单例对象只会在第一次使用时自动完成创建,之后所有对该对象的访问都会自动将已经创建好的对象注入进来。

常用的方法是 Singleton(initialize interface{}) error 方法,该方法会按照你提供的 initialize 函数或者对象来完成单例对象的注册。

参数 initialize 支持以下几种形式:

  • 对象创建函数 func(deps...) 对象返回值

    比如

      cc.Singleton(func() UserRepo { return &userRepoImpl{} })
      cc.Singleton(func() (*sql.DB, error) {
          return sql.Open("mysql", "user:pwd@tcp(ip:3306)/dbname")
      })
      cc.Singleton(func(db *sql.DB) UserRepo { 
          // 这里我们创建的 userRepoImpl 对象,依赖 sql.DB 对象,只需要在函数
          // 参数中,将依赖列举出来,容器会自动完成这些对象的创建
          return &userRepoImpl{db: db} 
      })
    
  • 带错误返回值的对象创建函数 func(deps...) (对象返回值, error)

    对象创建函数最多支持两个返回值,且要求第一个返回值为期望创建的对象,第二个返回值为 error 对象。

      cc.Singleton(func() (Config, error) {
          // 假设我们要创建配置对象,该对象的初始化时从文件读取配置
          content, err := ioutil.ReadFile("test.conf")
          if err != nil {
              return nil, err
          }
    
          return config.Load(content), nil
      })
    
  • 直接绑定对象

    如果对象已经创建好了,想要让 Container 来管理,可以直接将对象传递 Singleton 方法

      userRepo := repo.NewUserRepo()
      cc.Singleton(userRepo)
    

当对象第一次被使用时,Container 会将对象创建函数的执行结果缓存起来,从而实现任何时候后访问都是获取到的同一个对象。

原型对象(多例对象)

原型对象(多例对象)是指的由 Container 托管对象的创建过程,但是每次使用依赖注入获取到的都是新创建的对象。

使用 Prototype 系列的方法来将原型对象的创建托管给容器。常用的方法是 Prototype(initialize interface{}) error

参数 initialize 可以接受的类型与 Singleton 系列函数完全一致,唯一的区别是在对象使用时,单例对象每次都是返回的同一个对象,而原型对象则是每次都返回新创建的对象。

字符串值对象绑定

这种绑定方式是将某个对象绑定到 Container 中,但是与 Singleton 系列方法不同的是,它要求必须指定一个字符串类型的 Key,每次获取对象的时候,使用 Get 系列函数获取绑定的对象时,直接传递这个字符串 Key 即可。

常用的绑定方法为 BindValue(key string, value interface{})

cc.BindValue("version", "1.0.1")
cc.MustBindValue("startTs", time.Now())
cc.BindValue("int_val", 123)

依赖注入

在使用绑定对象时,通常我们使用 ResolveCall 系列方法。

Resolve

Resolve(callback interface{}) error 方法执行体 callback 内部只能进行依赖注入,不接收注入函数的返回值,虽然有一个 error 返回值,但是该值只表明是否在注入对象时产生错误。

比如,我们需要获取某个用户的信息和其角色信息,使用 Resolve 方法

cc.MustResolve(func(userRepo repo.UserRepo, roleRepo repo.RoleRepo) {
    // 查询 id=123 的用户,查询失败直接panic
    user, err := userRepo.GetUser(123)
    if err != nil {
        panic(err)
    }
    // 查询用户角色,查询失败时,我们忽略了返回的错误
    role, _ := roleRepo.GetRole(user.RoleID)

    // do something you want with user/role
})

直接使用 Resolve 方法可能并不太满足我们的日常业务需求,因为在执行查询的时候,总是会遇到各种 error,直接丢弃会产生很多隐藏的 Bug,但是我们也不倾向于使用 Panic 这种暴力的方式来解决。

Container 提供了 ResolveWithError(callback interface{}) error 方法,使用该方法时,我们的 callback 可以接受一个 error 返回值,来告诉调用者这里出现问题了。

err := cc.ResolveWithError(func(userRepo repo.UserRepo, roleRepo repo.RoleRepoo) error {
    user, err := userRepo.GetUser(123)
    if err != nil {
        return err
    }

    role, err := roleRepo.GetRole(user.RoleID)
    if err != nil {
        return err
    }

    // do something you want with user/role

    return nil
})
if err != nil {
    // 自定义错误处理
}

Call

Call(callback interface{}) ([]interface{}, error) 方法不仅完成对象的依赖注入,还会返回 callback 的返回值,返回值为数组结构。

比如

results, err := cc.Call(func(userRepo repo.UserRepo) ([]repo.User, error) {
    users, err := userRepo.AllUsers()
    return users, err
})
if err != nil {
    // 这里的 err 是依赖注入过程中的错误,比如依赖对象创建失败
}

// results 是一个类型为 []interface{} 的数组,数组中按次序包含了 callback 函数的返回值
// results[0] - []repo.User
// results[1] - error
// 由于每个返回值都是 interface{} 类型,因此在使用时需要执行类型断言,将其转换为具体的类型再使用
users := results[0].([]repo.User)
err := results[0].(error)

Provider

有时我们希望为不同的功能模块绑定不同的对象实现,比如在 Web 服务器中,每个请求的 handler 函数需要访问与本次请求有关的 request/response 对象,请求结束之后,Container 中的 request/response 对象也就没有用了,不同的请求获取到的也不是同一个对象。我们可以使用 CallWithProvider(callback interface{}, provider func() []*Entity) ([]interface{}, error) 配合 Provider(initializes ...interface{}) (func() []*Entity, error) 方法实现该功能。

ctxFunc := func() Context { return ctx }
requestFunc := func() Request { return ctx.request }

provider, _ := cc.Provider(ctxFunc, requestFunc)
results, err := cc.CallWithProvider(func(userRepo repo.UserRepo, req Request) ([]repo.User, error) {
    // 这里我们注入的 Request 对象,只对当前 callback 有效
    userId := req.Input("user_id")
    users, err := userRepo.GetUser(userId)
    
    return users, err
}, provider)

AutoWire 结构体属性注入

使用 AutoWire 方法可以为结构体的属性注入其绑定的对象,要使用该特性,我们需要在需要依赖注入的结构体对象上添加 autowire 标签。

type UserManager struct {
    UserRepo *UserRepo `autowire:"@" json:"-"`
    field1   string    `autowire:"version"`
    Field2   string    `json:"field2"`
}

manager := UserManager{}
// 对 manager 执行 AutoWire 之后,会自动注入 UserRepo 和 field1 的值
if err := c.AutoWire(&manager); err != nil {
    t.Error("test failed")
}

结构体属性注入支持公开和私有字段的注入。如果对象是通过类型来注入的,使用 autowire:"@" 来标记属性;如果使用的是 BindValue 绑定的字符串为key的对象,则使用 autowire:"Key名称" 来标记属性。

由于 AutoWire 要修改对象,因此必须使用对象的指针,结构体类型必须使用 &

其它方法

HasBound/HasBoundValue

方法签名

HasBound(key interface{}) bool
HasBoundValue(key string) bool

用于判断指定的 Key 是否已经绑定过了。

Keys

方法签名

Keys() []interface{}

获取所有绑定到 Container 中的对象信息。

CanOverride

方法签名

CanOverride(key interface{}) (bool, error)

判断指定的 Key 是否可以覆盖,重新绑定创建函数。

Extend

Extend 并不是 Container 实例上的一个方法,而是一个独立的函数,用于从已有的 Container 生成一个新的 Container,新的 Container 继承已有 Container 所有的对象绑定。

Extend(c Container) Container

容器继承之后,在依赖注入对象查找时,会优先从当前 Container 中查找,当找不到对象时,再从父对象查找。

在 Container 实例上个,有一个名为 ExtendFrom(parent Container) 的方法,该方法用于指定当前 Container 从 parent 继承。

示例项目

简单的示例可以参考项目的 example 目录。

以下项目中使用了 Container 作为依赖注入管理库,感兴趣的可以参考一下。

  • Glacier 一个应用管理框架,目前还没有写使用文档,该框架集成了 Container,用来管理框架的对象实例化。
  • Adanos-Alert 使用 Glacier 开发的一款报警系统,它侧重点并不是监控,而是报警,可以对各种报警信息进行聚合,按照配置规则来实现多样化的报警,一般用于配合 Logstash 来完成业务和错误日志的报警,配合PrometheusOpenFalcon 等主流监控框架完成服务级的报警。目前还在开发中,但基本功能已经可用。
  • Sync 使用 Glacier 开发一款跨主机文件同步工具,拥有友好的 web 配置界面,使用 GRPC 实现不同服务器之间文件的同步。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349