goioc:一個使用 Go 寫的簡易的 ioc 框架

痴者工良發表於2022-11-28

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 的形式都可以。

相關文章