Introduction to goioc
goioc is a dependency injection framework written in Go language that is based on reflection.
- Supports generics;
- Simple and easy-to-use API;
- Simplified object lifecycle management, objects have a life within the scope;
- Lazy loading, objects are instantiated only when needed;
- Supports struct field injection and multi-layer injection;
- Object instantiation is thread-safe and will be executed only once within the scope.
Download dependency:
go get -u github.com/whuanle/goioc v2.0.0
Quick Start
Define an interface:
type IAnimal interface {
Println(s string)
}
Implement the interface:
type Dog struct {
}
func (my Dog) Println(s string) {
fmt.Println(s)
}
Dependency injection and usage:
// Register container
var sc goioc.IServiceCollection = &ServiceCollection{}
// Inject service
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
// Build provider
p := sc.Build()
// Get service
obj := goioc.Get[IAnimal](p)
Interface Introduction
IServiceCollection
is a container interface that allows registration of objects that need dependency injection into the container.
IServiceProvider
is a service provider that manages the lifecycle of services and provides services after they have been registered to the container.
The IDispose
interface is used to declare that the object needs to be released when IServiceProvider
ends.
// IDispose release interface
type IDispose interface {
// Dispose releases resources
Dispose()
}
In addition, goioc also defines some extension functions, such as generic injection, with a small amount of code that is simple to use.
Using goioc
How to Use
There are two forms of injected services: the first is B:A
, meaning B implements A, and when using it, A is obtained; the second is injecting B and retrieving B when needed.
// First form
AddServiceOf[A,B]()
// Second form
AddService[B]()
A can be an interface or a struct, as long as B implements A.
Define an interface:
type IAnimal interface {
Println(s string)
}
Implement this interface:
type Dog struct {
Id int
}
func (my Dog) Println(s string) {
fmt.Println(s)
}
When using a dependency injection framework, we can separate the interface and implementation, and even place them in two different modules, allowing for easy replacement of the interface implementation.
Below is an example of registering and retrieving services:
func Demo() {
sc := &ServiceCollection{}
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
p := sc.Build()
animal := goioc.GetI[IAnimal](p)
animal.Println("test")
}
Now let's go through the coding process.
First, create an IServiceCollection
container, where services can be registered.
sc := &ServiceCollection{}
Then inject the service through the interface:
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
This function is a generic method. If generics are not used, the injection process can be much more cumbersome.
After the registration, build the provider:
p := sc.Build()
Then retrieve the service:
animal := goioc.GetI[IAnimal](p)
animal.Println("test")
Lifecycle
goioc defines three lifecycles:
const (
Transient ServiceLifetime = iota
Scope
Singleton
)
Transient
: transient mode, every time you retrieve a new object.
Scope
: scoped mode, within the same Provider, the retrieved object is the same.
Singleton
: singleton mode, the same ServiceCollection
retrieves the same object, meaning all Providers will get the same object.
In singleton mode (Singleton), regardless of how many times Build is called, the object is always the same:
When registering services, the object lifecycle needs to be specified.
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
In injection with a scope lifecycle, the same object is obtained within the same Provider.
sc := &ServiceCollection{}
goioc.AddServiceOf[IAnimal, Dog](sc, goioc.Scope)
p := sc.Build()
// First retrieval of the object
animal1 := goioc.GetI[IAnimal](p)
if animal1 == nil {
t.Errorf("service is nil!")
}
animal1.Println("test")
// Second retrieval of the object
animal2 := goioc.GetI[IAnimal](p)
if animal2 == nil {
t.Errorf("service is nil!")
}
// animal1 and animal2 reference the same object
if animal1 != animal2 {
t.Errorf("animal1 != animal2")
}
Instance one: Objects with a Scope lifecycle are the same when retrieved under the same provider.
sc := &ServiceCollection{}
goioc.AddServiceHandlerOf[IAnimal, Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
return &Dog{
Id: 3,
}
})
p := sc.Build()
// First retrieval
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
// Second retrieval
aa := goioc.GetI[IAnimal](p)
v = aa.(*Dog)
if v.Id != 3 {
t.Errorf("Life cycle error")
}
// Rebuilding the scope, not the same object
pp := sc.Build()
aaa := goioc.GetI[IAnimal](pp)
v = aaa.(*Dog)
if v.Id != 2 {
t.Errorf("Life cycle error")
}
Instance two: In the provider constructed by ServiceCollection
, objects retrieved in singleton mode are the same.
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")
}
Instantiation
Developers decide how to instantiate an object.
This is mainly determined by the registration form, with four generic functions implementing service registration:
// AddService registers an object
func AddService[T any](con IServiceCollection, lifetime ServiceLifetime)
// AddServiceHandler registers an object and customizes how to initialize instances
func AddServiceHandler[T any](con IServiceCollection, lifetime ServiceLifetime, f func(provider IServiceProvider) interface{})
// AddServiceOf registers an object, registering interface or parent type and its implementation; serviceType must implement baseType
func AddServiceOf[I any, T any](con IServiceCollection, lifetime ServiceLifetime)
// AddServiceHandlerOf registers an object, registering interface or parent type and its implementation, serviceType must implement baseType, and customizing how to initialize instances
func AddServiceHandlerOf[I any, T any](con IServiceCollection, lifetime ServiceLifetime, f func(provider IServiceProvider) interface{})
AddService[T any]
: Only registers instantiable objects:
AddService[T any]
goioc.AddService[Dog](sc, goioc.Scope)
AddServiceHandler
registers an interface or struct and customizes instantiation.
The func(provider goioc.IServiceProvider) interface{}
function will execute when an object is instantiated.
goioc.AddServiceHandler[Dog](sc, goioc.Scope, func(provider goioc.IServiceProvider) interface{} {
return &Dog{
Id: 1,
}
})
When instantiating, if this object has dependencies on other services, it can retrieve those dependencies via goioc.IServiceProvider
.
For example, in the following example, one object depends on another, and you can customize the instantiation function to retrieve other dependency objects from the container:
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,
}
}
})
Getting Objects
As mentioned earlier, we can inject [A,B]
, or [B]
.
Thus, there are three ways to retrieve:
// Get retrieves an object
func Get[T any](provider IServiceProvider) interface{}
// GetI retrieves an object by interface
func GetI[T interface{}](provider IServiceProvider) T
// GetS retrieves an object by struct
func GetS[T interface{} | struct{}](provider IServiceProvider) *T
Get[T any]
retrieves an interface or struct, returning interface{}
.
GetI[T interface{}]
retrieves an interface instance.
GetS[T interface{} | struct{}]
retrieves a struct instance.
All three methods return object references, i.e., pointers.
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)
Struct Field Dependency Injection
Fields in structs can automatically inject and convert instances.
For instance, in the definition of the struct Animal, when it uses other structs, goioc can automatically inject corresponding fields. Fields to be injected must be interfaces or structs.
// Struct containing other objects
type Animal struct {
Dog IAnimal `ioc:"true"`
}
Fields that require automatic injection must have a tag that includes
ioc:"true"
to be effective.
Example code:
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 can automatically perform dependency injection for the fields in your structs.
Note: The conversion logic for field injection in goioc is as follows.
If
obj
needs to be converted to an interface, it uses:animal := (*obj).(IAnimal)
If
obj
needs to be converted to a struct, it uses:animal := (*obj).(*Animal)
Dispose Interface
Using goioc with Reflection
How to Use
The principle of goioc is reflection. It uses a large amount of reflection mechanism to implement dependency injection. However, because Go's reflection is relatively difficult to use, it can make operations quite cumbersome. Therefore, wrapping it with generics can reduce the difficulty of use.
Of course, you can also directly use the raw reflection method for dependency injection.
First, use reflection to obtain reflect.Type
.
// Get reflect.Type
imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
my := reflect.TypeOf((*Dog)(nil)).Elem()
Dependency injection:
// Create container
sc := &ServiceCollection{}
// Inject service, lifecycle is scoped
sc.AddServiceOf(goioc.Scope, imy, my)
// Build service Provider
serviceProvider := sc.Build()
Retrieve the service and perform type conversion:
// Get object
// *interface{} = &Dog{}, therefore pointer handling is required
obj, err := serviceProvider.GetService(imy)
animal := (*obj).(IAnimal)
Example:
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()
// Get object
// *interface{} = &Dog{}, therefore pointer handling is required
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("The two objects are not the same")
}
Getting the reflect.Type of interfaces and structs:
// Method 1
// Interface reflect.Type
var animal IAnimal
imy := reflect.TypeOf(&animal).Elem()
my := reflect.TypeOf(Dog{})
// Method 2
// Get reflect.Type
imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
my := reflect.TypeOf((*Dog)(nil)).Elem()
Both methods can be used to obtain the reflect.Type of interfaces and structs. However, the first method instantiates the struct, consuming memory, and for obtaining the reflect.Type of the interface, it cannot directly use
reflect.TypeOf(animal)
, instead needingreflect.TypeOf(&animal).Elem()
.
Then inject the service, with the lifecycle set to Scoped:
// Inject service, lifecycle is scoped
sc.AddServiceOf(goioc.Scope, imy, my)
When you need the
IAnimal
interface,Dog
struct will be automatically injected toIAnimal
.
构建依赖注入服务提供器:
// Build the service Provider
serviceProvider := sc.Build()
构建完成后,即可通过 Provider 对象获取需要的实例:
// Get object
// *interface{}
obj, err := serviceProvider.GetService(imy)
if err != nil {
panic(err)
}
// Convert to interface
a := (*obj).(IAnimal)
// a := (*obj).(*Dog)
因为使用了依赖注入,我们使用时,只需要使用接口即可,不需要知道具体的实现。
完整的代码示例:
// Get reflect.Type
imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
my := reflect.TypeOf((*Dog)(nil)).Elem()
// Create container
sc := &ServiceCollection{}
// Inject service, with scope lifetime
sc.AddServiceOf(goioc.Scope, imy, my)
// Build the service Provider
serviceProvider := sc.Build()
// Get object
// *interface{} = &Dog{}
obj, err := serviceProvider.GetService(imy)
if err != nil {
panic(err)
}
fmt.Println("obj type is", reflect.ValueOf(obj).Type())
// *interface{} = &Dog{}; need to handle pointer
animal := (*obj).(IAnimal)
// a := (*obj).(*Dog)
animal.Println("Test")
接口、结构体、结构体指针
在结构体注入时,可以对需要的字段进行自动实例化赋值,而字段可能有以下情况:
// Field is an interface
type Animal1 struct {
Dog IAnimal `ioc:"true"`
}
// Field is a struct
type Animal2 struct {
Dog Dog `ioc:"true"`
}
// Field is a struct pointer
type Animal3 struct {
Dog *Dog `ioc:"true"`
}
首先注入前置的依赖对象:
// Get reflect.Type
imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
my := reflect.TypeOf((*Dog)(nil)).Elem()
// Create container
p := &ServiceCollection{}
// Inject service, with scope lifetime
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)
然后愉快地获取这些对象实例:
// Build the service 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 语言中结构体是值类型,因此给值类型赋值是,是值赋值,因此对象不是同一个了。
不会自动注入本身
下面是一个依赖注入过程:
// Get reflect.Type
imy := reflect.TypeOf((*IAnimal)(nil)).Elem()
my := reflect.TypeOf((*Dog)(nil)).Elem()
// Create container
sc := &ServiceCollection{}
// Inject service, with scope lifetime
sc.AddServiceOf(goioc.Scope, imy, my)
此时,注册的服务是 IAnimal,你只能通过 IAnimal 获取实例,无法通过 Dog 获取实例。
如果你想获取 Dog,需要自行注入:
// Inject service, with scope lifetime
p.AddServiceOf(goioc.Scope, imy, my)
p.AddService(my)
如果是结构体字段,则使用 IAnimal、Dog、
*Dog
的形式都可以。
文章评论