網站首頁 編程語言 正文
在現代的 web 框架里面,基本都有實現了依賴注入的功能,可以讓我們很方便地對應用的依賴進行管理,同時免去在各個地方 new 對象的麻煩。比如 Laravel
里面的 Application
,又或者 Java 的 Spring
框架也自帶依賴注入功能。
今天我們來看看 go 里面實現依賴注入的一種方式,以 flamego 里的 inject 為例子。
我們要了解一個軟件的設計,先要看它定義了一個什么樣的模型,但是在了解模型之前,我們更應該清楚了解,為什么會出現這個模型,也就是我們構建出了這個模型到底是為了解決什么問題。
依賴注入要解決的問題
我們先來看看,在沒有依賴注入之前,我們需要的依賴是如何構建出來的,假設有如下 struct
定義:
type A struct { } type B struct { a A } type C struct { b B } func test(c C) { println("c called") }
假設我們要調用 test
,就需要創建一個 C
的實例,而創建 C
的實例需要創建一個 B
的實例,而創建 B
的實例需要一個 A
的實例。如下是一個例子:
a := A{} b := B{a: a} c := C{b: b} test(c)
我們可以看到,這個過程非常的繁瑣,只有一個地方需要這樣調用 test
還好,如果有多個地方都需要調用 test
,那我們就要做很多創建實例的操作,而且一旦實例的構建過程發生變化,我們就需要改動很多地方。
所以現在的 web 框架里面一般都將這個實例化的過程固化下來,在框架的某個地方注冊一些實例化的函數,在我們需要的時候就調用之前注冊的實例化的函數,實例化之后,再根據需要看看是否需要將這個實例保留在內存里面,從而在免去了手動實例化的過程之外,節省我們資源的開銷(不用每次使用的時候都實例化一次)。
而這里說到的固化的實例化過程,其實就是我們本文所說的依賴注入。在 Laravel
里面我們可以通過 ServiceProvider
的 app()->register()
或者 app()->bind()
等函數來做依賴注入的一些操作。
inject 依賴注入模型/設計
以下是 Injector
的大概模型,Injector
接口里面嵌套了 Applicator
、Invoker
、TypeMapper
接口,之所以這樣做是出于接口隔離原則考慮,因為這三者代表了細化的三種不同功能,分離出不同的接口可以讓我們的代碼更加的清晰,也會更利于代碼的后續演進。
-
Injector
:依賴注入容器 -
Applicator
:結構體注入的接口 -
Invoker
:使用注入的依賴來調用函數 -
TypeMapper
:類型映射,需要特別注意的是,在Injector
里面,是通過類型來綁定依賴(不同于Laravel
的依賴注入容器可以通過字符串命名的方式來綁定依賴,當然將Injector
稍微改改也是可以實現的,就看有沒有這種需求罷了)。
// 依賴注入容器 type Injector interface { Applicator Invoker TypeMapper // 上一級 Injector SetParent(Injector) } // 給結構體字段注入依賴 type Applicator interface { Apply(interface{}) error } // 調用函數,Invoke 的參數是被調用的函數, // 這個函數的參數事先通過 Injector 注入, // 調用的時候從 Injector 里面獲取依賴 type Invoker interface { Invoke(interface{}) ([]reflect.Value, error) } // 往 Injector 注入依賴 type TypeMapper interface { Map(...interface{}) TypeMapper MapTo(interface{}, interface{}) TypeMapper Set(reflect.Type, reflect.Value) TypeMapper Value(reflect.Type) reflect.Value }
表示成圖像大概如下:
我們可以通過 Injector
的 TypeMapper
來往依賴注入容器里面注入依賴,然后在我們需要為結構體的字段注入依賴,又或者為函數參數注入依賴的時候,可以通過 Applicator
或者 Invoker
來實現注入依賴。
而 SetParent
這個方法比較有意思,它其實將 Injector
這個模型拓展了,形成了一個有父子關系的模型。在其他語言里面可能作用不是很明顯,但是在 go 里面,這個父子模型恰好和 go 的協程的父子模型一致。在 go 里面,我們可以在一個協程里面再創建一個 Injector
,然后在這里面定義一些在當前協程以及當前協程子協程可以用到的一些依賴,而不用影響外部的 Injector
。
當然上面說到的協程只是 Injector
里面 SetParent
的一種用法,另外一種用法是,我們的 web 應用往往會根據路由前綴來劃分為不同的組,而這種路由組的結構組織方式其實也是一種父子結構,在這種場景下,我們就可以針對全局注入一些依賴的情況下,再針對某個路由組來注入路由組特定的依賴。
injector 的依賴注入實現
我們來看看 injector
的結構體:
type injector struct { // 注入的依賴 values map[reflect.Type]reflect.Value // 上級 Injector parent Injector }
這個結構體定義很簡單,就只有兩個字段,values
和 parent
,我們通過 TypeMapper
注入的依賴都保存在 values
里面,values
是通過反射來記錄我們注入的參數類型和值的。
那我們是如何注入依賴的呢?再來看看 TypeMapper
的 Map
方法:
func (inj *injector) Map(values ...interface{}) TypeMapper { for _, val := range values { inj.values[reflect.TypeOf(val)] = reflect.ValueOf(val) } return inj }
我們可以看到,對于傳入給 Map
的參數,這里獲取了它的反射類型作為 values
map 的 key,而獲取了傳入參數的反射值作為 values
里面 map 的值。其他的兩個方法 MapTo
、Set
也是類似的功能,最終的效果都是獲取依賴的類型作為 values 的 key,依賴的值作為 values 的值。
到此為止,我們知道 Injector
是如何注入依賴的了。
那么它又是如何去從依賴注入容器里面拿到我們注入的數據的呢?又是如何使用這些數據的呢?
我們再來看看 callInvoke
方法(也就是 Injector
的 Invoke
實現):
func (inj *injector) callInvoke(f interface{}, t reflect.Type, numIn int) ([]reflect.Value, error) { // 參數切片,用來保存從 Injector 里面獲取的依賴 var in []reflect.Value // 只有 f 有參數的時候,才需要從 Injector 獲取依賴 if numIn > 0 { // 初始化切片 in = make([]reflect.Value, numIn) var argType reflect.Type var val reflect.Value // 遍歷 f 參數 for i := 0; i < numIn; i++ { // 獲取 f 參數類型 argType = t.In(i) // 從 Injector 獲取該類型對應的依賴 val = inj.Value(argType) // 如果函數參數未注入,則調用出錯 if !val.IsValid() { return nil, fmt.Errorf("value not found for type %v", argType) } // 保存從 Injector 獲取到的值 in[i] = val } } // 通過反射調用 f 函數,in 是參數切片 return reflect.ValueOf(f).Call(in), nil }
參數和返回值說明:
- 第一個參數是我們 Invoke 的函數,這個函數的參數,都會通過 Injector 根據函數參數類型獲取
- 第二個參數 f 的反射類型,也就是 reflect.TypeOf(f)
- 第三個參數是 f 的參數個數
- 返回值是
reflect.Value
切片,如果我們在調用過程出錯,返回error
在這個函數中,會通過反射來獲取 f
的參數類型(reflect.Type
),拿到這個類型之后,從 Injector
里面獲取我們之前注入的依賴,這樣我們就可以拿到所有參數對應的值。最后,通過 reflect.ValueOf(f)
來調用 f
函數,參數是我們從 Injector
獲取到的值的切片。調用之后,返回函數調用結果,一個 reflect.Value
切片。
當然,這只是其中一種使用依賴的方式,另外一種方式也比較常見,就是為結構體注入依賴,這跟 hyperf
里面通過注釋注解又或者 Spring
里面的注入方式有點類似。在 Injector
里面是通過 Apply
來為結構體字段注入依賴的:
// 參數 val 是待注入依賴的結構體 func (inj *injector) Apply(val interface{}) error { v := reflect.ValueOf(val) // 獲取底層元素 for v.Kind() == reflect.Ptr { v = v.Elem() } // 底層類型不是結構體則返回 if v.Kind() != reflect.Struct { return nil // Should not panic here ? } // v 的反射類型 t := v.Type() // 遍歷結構體的字段 for i := 0; i < v.NumField(); i++ { // 獲取第 i 個結構體字段 // v 的類型是 reflect.Value // v.Field 返回的是結構體字段的值 f := v.Field(i) // t 的類型是 *reflect.rtype // t.Field 返回的是 reflect.Type,是類型信息 structField := t.Field(i) // 檢查是否有 inject tag,有這個 tag 才會進行依賴注入 _, ok := structField.Tag.Lookup("inject") // 字段支持反射設置,并且存在 inject tag 才會進行注入 if f.CanSet() && ok { // 通過反射類型從 Injector 中獲取對應的值 ft := f.Type() v := inj.Value(ft) // 獲取不到注入的依賴,則返回錯誤 if !v.IsValid() { return fmt.Errorf("value not found for type %v", ft) } // 設置結構體字段值 f.Set(v) } } return nil }
簡單來說,Injector
里面,通過 TypeMapper
來注入依賴,然后通過 Apply
或者 Invoke
來使用注入的依賴。
例子
還是以一開始的例子為例,通過依賴注入的方式來改造一下:
a := A{} b := B{a: a} c := C{b: b} // 新建依賴注入容器 inj := injector{ values: make(map[reflect.Type]reflect.Value), } // 注入依賴 c inj.Map(c) // 調用函數 test,test 的參數 `C` 會通過依賴注入容器獲取 _, _ = inj.Invoke(test) // 輸出 "c called"
這個例子中,我們通過 inj.Map
來注入了依賴,在后續通過 inj.Invoke
來調用 test
函數的時候,將會從依賴注入容器里面獲取 test
的參數,然后將這些參數傳入 test
來調用。
這個例子也許比較簡單,但是如果我們很多地方都需要用到 C
這個參數的話,我們通過 inj.Invoke
的方式來調用函數就可以避免每一次調用都要實例化 C
的繁瑣操作了。
原文鏈接:https://juejin.cn/post/7175340508552626235
相關推薦
- 2023-01-06 linux?find命令將查找到的文件批量刪除方法_linux shell
- 2022-12-22 C++?push方法與push_back方法常見方法介紹_C 語言
- 2022-12-14 Go語言defer的一些神奇規則示例詳解_Golang
- 2022-06-25 python實現人機對戰的井字棋游戲_python
- 2022-12-24 Android全面屏適配方法詳解_Android
- 2023-01-05 Pandas使用Merge與Join和Concat分別進行合并數據效率對比分析_python
- 2022-02-20 千分位保留兩位小數,出現“toFixed() is not a function”的解決辦法
- 2022-04-01 Python?eval()?函數看這一篇就夠了_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支