Container 一款為 Go 開發的依賴注入容器

mylxsw發表於2020-05-25

Container 是一款為 Go 語言開發的執行時依賴注入庫。Go 語言的語言特性決定了實現一款型別安全的依賴注入容器並不太容易,因此 Container 大量使用了 Go 的反射機制。如果你的使用場景對效能要求並不是那個苛刻,那 Container 非常適合你。

並不是說對效能要求苛刻的環境中就不能使用了,你可以把 Container 作為一個物件依賴管理工具,在你的業務初始化時獲取依賴的物件。

使用方式

go get github.com/mylxsw/container

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

cc := container.New()

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

你也可以使用 container.NewWithContext(ctx) 來建立容器,建立之後,可以自動的把已經存在的 context.Context 物件新增到容器中,由容器託管。

物件繫結

在使用之前,我們需要先將我們要託管的物件告訴容器。Container 支援三種型別的物件管理

  • 單例物件 Singleton
  • 原型物件(多例物件) Prototype
  • 字串值物件繫結 Value

所有的物件繫結方法都會返回一個 error 返回值來說明是否繫結成功,應用在使用時一定要主動去檢查這個 error

確定物件一定會繫結成功(一般不違反文件中描述的引數簽名方式,都是一定會成功的)或者要求物件必須要繫結成功(通常我們都要求這樣,不然怎麼進行依賴管理呢),則可以使用 Must 系列方法,比如 Singleton 方法對應的時 MustSingleton,當建立出錯時,該方法會直接 panic

繫結物件時,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 實現不同伺服器之間檔案的同步。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章