goioc:一个使用 Go 写的简易的 ioc 框架

简介: goioc 介绍快速上手接口介绍使用 goioc如何使用生命周期实例化获取对象结构体字段依赖注入Dispose 接口反射形式使用 goioc如何使用接口、结构体、结构体指针不会自动注入本身

goioc 介绍

goioc 是一个基于 GO 语言编写的依赖注入框架,基于反射进行编写。

  • 支持泛型;
  • 简单易用的 API;
  • 简易版本的对象生命周期管理,作用域内对象具有生命;
  • 延迟加载,在需要的时候才会实例化对象;
  • 支持结构体字段注入,多层注入;
  • 对象实例化线程安全,作用域内只会被执行一次。

下载依赖:

go get -u github.com/whuanle/goioc v2.0.0

快速上手

定义接口:

type IAnimal interface {
  Println(s string)
}

实现接口:

type Dog struct {
}
func (my Dog) Println(s string) {
  fmt.Println(s)
}

依赖注入以及使用:

// 注册容器
var sc goioc.IServiceCollection = &ServiceCollection{}
// 注入服务
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
// 构建提供器
p := sc.Build()
// 获取服务
obj := goioc.Get[IAnimal](p)

接口介绍

IServiceCollection 是一个容器接口,通过此接口,将需要进行依赖注入的对象注册到容器中。

IServiceProvider 是一个服务提供器,当服务注册到容器后,构建一个服务提供器,IServiceProvider 可以管理服务的生命周期以及提供服务。

IDispose 接口用于声明此对象在 IServiceProvider 结束时,需要执行接口释放对象。

// IDispose 释放接口
type IDispose interface {
  // Dispose 释放资源
  Dispose()
}

除此之外,goioc 中还定义了部分扩展函数,如泛型注入等,代码量不多,简单易用。

使用 goioc

如何使用

注入的服务有两种形式,第一种是 B:A,即 B 实现了 A,使用的时候获取 A ;第二种是注入 B,使用的时候获取 B。

// 第一种
AddServiceOf[A,B]()
// 第二种
AddService[B]()

A 可以是接口或结构体,只要 B 实现了 A 即可。

定义一个接口:

type IAnimal interface {
  Println(s string)
}

实现这个接口:

type Dog struct {
  Id int
}
func (my Dog) Println(s string) {
  fmt.Println(s)
}

当使用依赖注入框架时,我们可以将接口和实现分开,甚至放到两个模块中,可以随时替换接口的实现。

注册服务和获取服务的代码示例如下:

func Demo() {
  sc := &ServiceCollection{}
  goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
  p := sc.Build()
  animal := goioc.GetI[IAnimal](p)
  animal.Println("test")
}

下面讲解编码过程。

首先创建 IServiceCollection 容器,容器中可以注册服务。

sc := &ServiceCollection{}

然后通过接口注入服务:

goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)

这个函数是泛型方法。如果不使用泛型,则注入过程麻烦得多。

注册完毕后,开始构建提供器:

p := sc.Build()

然后获取服务:

animal := goioc.GetI[IAnimal](p)
  animal.Println("test")

生命周期

goioc 中定义了三个生命周期:

const (
  Transient ServiceLifetime = iota
  Scope
  Singleton
)

Transient:瞬时模式,每次获取到的都是新的对象;

Scope:作用域模式,同一个 Provider 中获取到的是同一个对象。

Singleton:单例模式,同一个 ServiceCollection 获取到的是同一个对象,也就是所有 Provider 获取到的都是同一个对象。

如果是单例模式(Singleton),那么无论多少次 Build,对象始终是同一个:

在注册服务的时候,需要注明对象生命周期。

goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)

生命周期为 scope 的注入,同一个 Provider 中,获取到的对象是一样的。

sc := &ServiceCollection{}
  goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
  p := sc.Build()
    // 第一次获取对象
  animal1 := goioc.GetI[IAnimal](p)
  if animal1 == nil {
    t.Errorf("service is nil!")
  }
  animal1.Println("test")
    // 第二次获取对象
  animal2 := goioc.GetI[IAnimal](p)
  if animal2 == nil {
    t.Errorf("service is nil!")
  }
    // animal1 和 animal2 引用了同一个对象
  if animal1 != animal2 {
    t.Errorf("animal1 != animal2")
  }

实例一,Scope 生命周期的对象,在同一个提供器下获取到的都是同一个对象。

sc := &ServiceCollection{}
    goioc.AddServiceHandlerOf[IAnimal, Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
    return &Dog{
      Id: 3,
    }
  })
  p := sc.Build()
  // 第一次获取
  a := goioc.GetI[IAnimal](p)
  if v := a.(*Dog); v == nil {
    t.Errorf("service is nil!")
  }
  v := a.(*Dog)
  if v.Id != 2 {
    t.Errorf("Life cycle error")
  }
  v.Id = 3
  // 第二次获取
  aa := goioc.GetI[IAnimal](p)
  v = aa.(*Dog)
  if v.Id != 3 {
    t.Errorf("Life cycle error")
  }
  // 重新构建的 scope,不是同一个对象
  pp := sc.Build()
  aaa := goioc.GetI[IAnimal](pp)
  v = aaa.(*Dog)
  if v.Id != 2 {
    t.Errorf("Life cycle error")
  }

实例二, ServiceCollection 构建的提供器,单例模式下获取到的都是同一个对象。

sc := &ServiceCollection{}
  goioc.AddServiceHandler[Dog](sc, goioc.Singleton, func(provider goioc.IServiceProvider) interface{} {
    return &Dog{
      Id: 2,
    }
  })
  p := sc.Build()
  b := goioc.GetS[Dog](p)
  if b.Id != 2 {
    t.Errorf("Life cycle error")
  }
  b.Id = 3
  bb := goioc.GetS[Dog](p)
  if b.Id != bb.Id {
    t.Errorf("Life cycle error")
  }
  ppp := sc.Build()
  bbb := goioc.GetS[Dog](ppp)
  if b.Id != bbb.Id {
    t.Errorf("Life cycle error")
  }

实例化

由开发者决定如何实例化一个对象。

主要由注册形式决定,有四个泛型函数实现注册服务:

// AddService 注册对象
func AddService[T any](con IServiceCollection, lifetime ServiceLifetime)
// AddServiceHandler 注册对象,并自定义如何初始化实例
func AddServiceHandler[T any](con IServiceCollection, lifetime ServiceLifetime, f func(provider IServiceProvider) interface{})
// AddServiceOf 注册对象,注册接口或父类型及其实现,serviceType 必须实现了 baseType
func AddServiceOf[I any, T any](con IServiceCollection, lifetime ServiceLifetime)
// AddServiceHandlerOf 注册对象,注册接口或父类型及其实现,serviceType 必须实现了 baseType,并自定义如何初始化实例
func AddServiceHandlerOf[I any, T any](con IServiceCollection, lifetime ServiceLifetime, f func(provider IServiceProvider) interface{})

AddService[T any]:只注册可被实例化的对象:

AddService[T any]
goioc.AddService[Dog](sc, goioc.Scope)

AddServiceHandler 注册一个接口或结构体,自定义实例化。

func(provider goioc.IServiceProvider) interface{} 函数会在实例化对象时执行。

goioc.AddServiceHandler[Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
    return &Dog{
      Id: 1,
    }
  })

在实例化时,如果这个对象还依赖其他服务,则可以通过 goioc.IServiceProvider 来获取其他依赖。

例如下面示例中,一个依赖另一个对象,可以自定义实例化函数,从容器中取出其他依赖对象,然后构建一个新的对象。

goioc.AddServiceHandler[Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
    a := goioc.GetI[IA](provider)
    return &Dog{
      Id: 1,
            A:  a,
    }
  })
goioc.AddServiceHandler[Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
    config := goioc.GetI[Config](provider)
    if config.Enable == false
    return &Dog{
      Id: 1,
    }
  })

获取对象

前面提到,我们可以注入 [A,B],或者 [B]

那么获取的时候就有三种函数:

// Get 获取对象
func Get[T any](provider IServiceProvider) interface{} 
// GetI 根据接口获取对象
func GetI[T interface{}](provider IServiceProvider) T 
// GetS 根据结构体获取对象
func GetS[T interface{} | struct{}](provider IServiceProvider) *T

Get[T any] 获取接口或结构体,返回 interface{}

GetI[T interface{}] 获取的是一个接口实例。

GetS[T interface{} | struct{}] 获取的是一个结构体实例。

以上三种方式,返回的都是对象的引用,即指针。

sc := &ServiceCollection{}
  goioc.AddService[Dog](sc, goioc.Scope)
  goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
  p := sc.Build()
  a := goioc.Get[IAnimal](p)
  b := goioc.Get[Dog](p)
  c := goioc.GetI[IAnimal](p)
  d := goioc.GetS[Dog](p)

结构体字段依赖注入

结构体中的字段,可以自动注入和转换实例。

如结构体 Animal 的定义,其使用了其它结构体,goioc 可以自动注入 Animal 对应字段,要被注入的字段必须是接口或者结构体。

// 结构体中包含了其它对象
type Animal struct {
  Dog IAnimal `ioc:"true"`
}

要对需要自动注入的字段设置 tag 中包含ioc:"true" 才会生效。

示例代码:

sc := &ServiceCollection{}
  goioc.AddServiceHandlerOf[IAnimal, Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
    return &Dog{
      Id: 666,
    }
  })
  goioc.AddService[Animal](sc, goioc.Scope)
  p := sc.Build()
  a := goioc.GetS[Animal](p)
  if dog := a.Dog.(*Dog); dog.Id != 666 {
    t.Errorf("service is nil!")
  }

goioc 可以自动给你的结构体字段进行自动依赖注入。

注意,goioc 的字段注入转换逻辑是这样的。

如果 obj 要转换为接口,则是使用:

animal := (*obj).(IAnimal)

如果 obj 要转换为结构体,则是:

animal := (*obj).(*Animal)

Dispose 接口

反射形式使用 goioc

如何使用

goioc 的原理是反射,ioc 使用了大量的反射机制实现依赖注入,但是因为 Go 的反射比较难用,导致操作十分麻烦,因此使用泛型包装一层可以降低使用难度。

当然,也可以直接使用原生的反射方式进行依赖注入。

首先反射通过反射获取 reflect.Type

// 获取 reflect.Type
  imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()

依赖注入:

// 创建容器
  sc := &ServiceCollection{}
  // 注入服务,生命周期为 scoped
  sc.AddServiceOf(goioc.Scope, imy, my)
  // 构建服务 Provider
  serviceProvider := sc.Build()

获取服务以及进行类型转换:

// 获取对象
  // *interface{} = &Dog{},因此需要处理指针
  obj, err := serviceProvider.GetService(imy)
  animal := (*obj).(IAnimal)

示例:

imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()
  var sc IServiceCollection = &ServiceCollection{}
  sc.AddServiceOf(goioc.Scope,imy, my)
  p := sc.Build()
  // 获取对象
  // *interface{} = &Dog{},因此需要处理指针
  obj1, _ := p.GetService(imy)
  obj2, _ := p.GetService(imy)
  fmt.Printf("obj1 = %p,obj2 = %p\r\n", (*obj1).(*Dog), (*obj2).(*Dog))
  if fmt.Sprintf("%p",(*obj1).(*Dog)) != fmt.Sprintf("%p",(*obj2).(*Dog)){
    t.Error("两个对象不是同一个")
  }

获取接口和结构体的 reflect.Type:

// 写法 1
    // 接口的 reflect.Type
  var animal IAnimal
    imy := reflect.TypeOf(&animal).Elem()
  my := reflect.TypeOf(Dog{})
// 写法 2
  // 获取 reflect.Type
  imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()

以上两种写法都可以使用,目的在于获取到接口和结构体的 reflect.Type。不过第一种方式会实例化结构体,消耗了一次内存,并且要获取接口的 reflect.Type,是不能直接有用 reflect.TypeOf(animal) 的,需要使用 reflect.TypeOf(&animal).Elem()

然后注入服务,其生命周期为 Scoped:

// 注入服务,生命周期为 scoped
  sc.AddServiceOf(goioc.Scope, imy, my)

当你需要 IAnimal 接口时,会自动注入 Dog 结构体给 IAnimal。

构建依赖注入服务提供器:

// 构建服务 Provider
  serviceProvider := sc.Build()

构建完成后,即可通过 Provider 对象获取需要的实例:

// 获取对象
  // *interface{}
  obj, err := serviceProvider.GetService(imy)
  if err != nil {
    panic(err)
  }
  // 转换为接口
  a := (*obj).(IAnimal)
  //  a := (*obj).(*Dog)

因为使用了依赖注入,我们使用时,只需要使用接口即可,不需要知道具体的实现。

完整的代码示例:

// 获取 reflect.Type
  imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()
  // 创建容器
  sc := &ServiceCollection{}
  // 注入服务,生命周期为 scoped
  sc.AddServiceOf(goioc.Scope, imy, my)
  // 构建服务 Provider
  serviceProvider := sc.Build()
  // 获取对象
  // *interface{} = &Dog{}
  obj, err := serviceProvider.GetService(imy)
  if err != nil {
    panic(err)
  }
  fmt.Println("obj 类型是", reflect.ValueOf(obj).Type())
  // *interface{} = &Dog{},因此需要处理指针
  animal := (*obj).(IAnimal)
  //  a := (*obj).(*Dog)
  animal.Println("测试")

接口、结构体、结构体指针

在结构体注入时,可以对需要的字段进行自动实例化赋值,而字段可能有以下情况:

// 字段是接口
type Animal1 struct {
  Dog IAnimal `ioc:"true"`
}
// 字段是结构体
type Animal2 struct {
  Dog Dog `ioc:"true"`
}
// 字段是结构体指针
type Animal3 struct {
  Dog *Dog `ioc:"true"`
}

首先注入前置的依赖对象:

// 获取 reflect.Type
  imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()
  // 创建容器
    p := &ServiceCollection{}
  // 注入服务,生命周期为 scoped
  p.AddServiceOf(goioc.Scope,imy, my)
    p.AddService(goioc.Scope, my)

然后将我们的一些对象注入进去:

t1 := reflect.TypeOf((*Animal1)(nil)).Elem()
  t2 := reflect.TypeOf((*Animal2)(nil)).Elem()
  t3 := reflect.TypeOf((*Animal3)(nil)).Elem()
  p.Ad(t1)
  p.AddServiceOf(goioc.Scope,t2)
  p.AddServiceOf(goioc.Scope,t3)

然后愉快地获取这些对象实例:

// 构建服务 Provider
  p := collection.Build()
  v1, _ := p.GetService(t1)
  v2, _ := p.GetService(t2)
  v3, _ := p.GetService(t3)
  fmt.Println(*v1)
  fmt.Println(*v2)
  fmt.Println(*v3)

打印对象信息:

&{0x3abdd8}
&{{}}
&{0x3abdd8}

可以看到,当你注入实例后,结构体字段可以是接口、结构体或结构体指针,goioc 会根据不同的情况注入对应的实例。

前面提到了对象是生命周期,这里有些地方需要注意。

如果字段是接口和结构体指针,那么 scope 生命周期时,注入的对象是同一个,可以参考前面的 v1、v3 的 Dog 字段,Dog 字段类型虽然不同,但是因为可以存储指针,因此注入的对象是同一个。如果字段是结构体,由于 Go 语言中结构体是值类型,因此给值类型赋值是,是值赋值,因此对象不是同一个了。

不会自动注入本身

下面是一个依赖注入过程:

// 获取 reflect.Type
  imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
  my := reflect.TypeOf((*Dog)(nil)).Elem()
  // 创建容器
    sc := &ServiceCollection{}
  // 注入服务,生命周期为 scoped
  sc.AddServiceOf(goioc.Scope,imy, my)

此时,注册的服务是 IAnimal,你只能通过 IAnimal 获取实例,无法通过 Dog 获取实例。

如果你想获取 Dog,需要自行注入:

// 注入服务,生命周期为 scoped
  p.AddServiceOf(goioc.Scope,imy, my)
  p.AddService(my)

如果是结构体字段,则使用 IAnimal、Dog、*Dog 的形式都可以。

相关文章
|
5月前
|
人工智能 测试技术 Go
Go 语言的主流框架
本文全面解析了 Go 语言主流技术生态,涵盖 Web 框架、微服务、数据库工具、测试与部署等多个领域。重点介绍了 Gin、Echo、Beego 等高性能框架,以及 gRPC-Go、Go-Micro 等微服务组件。同时分析了 GORM、Ent 等 ORM 工具与测试部署方案,并结合场景提供选型建议,助力开发者构建高效稳定的 Go 应用。
1277 0
|
3月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
299 86
|
2月前
|
JavaScript 前端开发 Java
【GoWails】Go做桌面应用开发?本篇文章带你上手Wails框架!一步步带你玩明白前后端双端的数据绑定!
wails是一个可以让你使用Go和Web技术编写桌面应用的项目 可以将它看作Go的快并且轻量级的Electron替代品。可以使用Go的功能,并结合现代化UI完成桌面应用程序的开发
494 4
|
2月前
|
开发框架 前端开发 Go
【GoGin】(0)基于Go的WEB开发框架,GO Gin是什么?怎么启动?本文给你答案
Gin:Go语言编写的Web框架,以更好的性能实现类似Martini框架的APInet/http、Beego:开源的高性能Go语言Web框架、Iris:最快的Go语言Web框架,完备的MVC支持。
372 1
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
6月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
10月前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
1310 7
|
10月前
|
存储 开发框架 Devops
eino — 基于go语言的大模型应用开发框架(一)
Eino 是一个受开源社区优秀LLM应用开发框架(如LangChain和LlamaIndex)启发的Go语言框架,强调简洁性、可扩展性和可靠性。它提供了易于复用的组件、强大的编排框架、简洁明了的API、最佳实践集合及实用的DevOps工具,支持快速构建和部署LLM应用。Eino不仅兼容多种模型库(如OpenAI、Ollama、Ark),还提供详细的官方文档和活跃的社区支持,便于开发者上手使用。
2131 8
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
314 5