Container - 爲 Go語言而生的運行時依賴注入容器

Container 是一款爲 Go 語言開發的運行時依賴注入庫。Go 語言的語言特性決定了實現一款類型安全的依賴注入容器並不太容易,所以 Container 大量使用了 Go 的反射機制。若是你的使用場景對性能要求並非那個苛刻,那 Container 很是適合你。mysql

並非說對性能要求苛刻的環境中就不能使用了,你能夠把 Container 做爲一個對象依賴管理工具,在你的業務初始化時獲取依賴的對象。

使用方式git

go get github.com/mylxsw/container

要建立一個 Container 實例,使用 containier.New 方法github

cc := container.New()

此時就建立了一個空的容器。web

你也可使用 container.NewWithContext(ctx) 來建立容器,建立以後,能夠自動的把已經存在的 context.Context 對象添加到容器中,由容器託管。

對象綁定

在使用以前,咱們須要先將咱們要託管的對象告訴容器。Container 支持三種類型的對象管理sql

  • 單例對象 Singleton
  • 原型對象(多例對象) Prototype
  • 字符串值對象綁定 Value
全部的對象綁定方法都會返回一個 error 返回值來講明是否綁定成功,應用在使用時必定要主動去檢查這個 error

肯定對象必定會綁定成功(通常不違反文檔中描述的參數簽名方式,都是必定會成功的)或者要求對象必需要綁定成功(一般咱們都要求這樣,否則怎麼進行依賴管理呢),則可使用 Must 系列方法,好比 Singleton 方法對應的時 MustSingleton,當建立出錯時,該方法會直接 panicjson

綁定對象時,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 實現不一樣服務器之間文件的同步。
相關文章
相關標籤/搜索