Golang 依賴注入:dig

【導讀】本文介紹依賴注入庫 dig。

一、簡介

go 是否需要依賴注入庫曾經是一個飽受爭議的話題。實際上是否需要依賴注入,取決於編程風格。依賴注入是一種編程模式。比較適合面向對象編程,在函數式編程中則不需要。go 是一門支持多範式編程的語言,所以在使用面向對象的大型項目中,還是建議按照實際情況判斷是否應該使用依賴注入模式。

二、主流的依賴注入庫

依賴注入實現的是一件很小的事情,所以單純實現依賴注入的軟件包稱不上框架,而只能被稱爲庫。目前主流的 golang 庫非常多,比如 uber 開源的 dig、elliotchance 開源的 dingo、sarulabs 開源的 di、google 開源的 wire 和 facebook 開源的 inject 等等。目前最受歡迎的是 dig 和 wire,這篇文章主要介紹 dig 的用法。

三、基本用法

創建容器

container := dig.New()

容器用來管理依賴。

注入依賴

調用容器的 Provide 方法,傳入一個工廠函數,容器會自動調用該工廠函數創建依賴,並保存到 container 中。

type DBClient struct {}

func NewDBClient() {
    return *DBClient{}
}

func InitDB() *DBClient {
    return NewDBClient()
}

container.Provide(InitDB)

注入的依賴會被 dig 所管理,每種類型的對象只會被創建一次,可以理解爲單例。如果再注入同一類型的依賴,工廠函數則不會被執行。

type DBClient struct {}

func NewDBClient() {
    return *DBClient{}
}

func InitDB() *DBClient {
    return NewDBClient()
}

func InitDB2() *DBClient {
    return NewDBClient()
}

container.Provide(InitDB)
container.Provide(InitDB2)// 不會執行

使用依賴

如果需要使用依賴,使用 container 的 Invoke 方法。並在函數的形式參數中定義參數,container 會自動把單例注入。

func UseOption(db *DBClient){

}

container.Invoke(UseOption)

四、參數對象

當某個函數所需要的依賴過多時,可以將參數以對象的方式獲取。假設啓動一個服務,需要 ServerConfig、MySQL、Redis、Mongodb 等參數。

container.Provide(func InitHttpServer(svcfg *ServerConfig, mysql *MySQL, redis *Redis, mongodb *Mongodb) *Server{
    // 業務邏輯
    return Server.Run()
})

此時代碼可讀性會變差,通過 dig.In 將 InitHttpServer 所依賴的 4 個依賴打包成一個對象。

type ServerParams {
    dig.In

    svcfg   *ServerConfig
    mysql   *MySQL
    redis   *Redis
    mongodb *Mongodb
}

container.Provide(func InitHttpServer(p ServerParams) *Server{
    // 業務邏輯
    return Server.Run()
})

五、結果對象

和參數對象類似,如果一個工廠函數返回了多個依賴時,會有相同的問題。不過這種情況比較少見。假設有個函數返回了啓動 InitHttpServer 所需要的所有依賴。

container.Provide(func BuildServerdependences() (*ServerConfig, *MySQL, *Redis, *Mongodb){
    // 業務邏輯
    return svcfg, mysql, redis, mongodb
})

解決這個現象的方式和 dig.In 類似,還有一個 dig.Out,用法一致。

type BuildServerdependences struct {
    dig.Out

    ServerConfig  *ServerConfig
    MySQL         *MySQL
    Redis         *Redis
    Mongodb       *Mongodb
}

container.Provide(func BuildServerdependences() (*ServerConfig, *MySQL, *Redis, *Mongodb){
    // 業務邏輯
    return BuildServerdependences{
        ServerConfig: svcfg,
        MySQL:        mysql,
        Redis:        redis, 
        Mongodb:      mongodb,
    }
})

六、可選依賴

如果在 Provide 的工廠函數或者 Invoke 的函數中所需要的依賴不存在,dig 會拋出異常。假設 container 中沒有 Mongo.Config 類型的依賴,那麼就會拋出異常。

func InitDB(cfg *Mongo.Config) *Mongo.Client{
    return Mongo.NewClient(cfg)
}

container.Invoke(InitDB)// 拋出異常

通過在標籤後標註 optional 爲 true 的方式可以允許某個依賴不存在,這時傳入的是 nil。

type InitDBParams struct {
    dig.In

    mongoConfig *Mongo.Config `optional:"true"`
}

func InitDB(p InitDBParams) *Mongo.Client {
    // p.mongoConfig 是 nil
    return Mongo.NewClient(cfg)
}

container.Invoke(InitDB)// 繼續執行

七、命名

注入命名依賴

由於默認是單例的,如果需要兩個相同類型的實例怎麼辦?比如現在需要兩臺 Mongodb 客戶端。dig 提供了對象命名功能,在調用 Provide 時傳入第二個參數就可以進行區分。

type MongodbClient struct {}

func NewMongoClient(cfg *Mongo.Client) *MongodbClient{
    return ConnectionMongo(cfg)
}

container.Provide(NewMongoClient, dig.Name("mgo1"))
container.Provide(NewMongoClient, dig.Name("mgo2"))

除了傳遞 dig.Name 參數以外,如果使用了結果對象的話,可以通過設置 name 標籤來實現,效果一致。

type MongodbClients struct {
    dig.In

    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}

使用命名依賴

不論是 Provide 還是 Invoke,都只能通過參數對象的方式使用命名依賴。使用方式是通過 tag,和注入時一致。

type MongodbClients struct {
    dig.Out

    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}

container.Invoke(func (mcs MongodbClients) {
    mcs.Mgo1
    mcs.Mgo2
})

注意事項

嵌套 dig.In 的結構體的所有字段必須都在 container 中存在,否則 Invoke 中傳入的函數不會被調用。錯誤示例:

type MongodbClients struct {
    dig.Out

    Mgo1 *MongodbClient `name:"mgo1"`
    Mgo2 *MongodbClient `name:"mgo2"`
}

container.Provide(NewMongoClient, dig.Name("mgo1"))// 只注入了 mgo1

container.Invoke(func (mcs MongodbClients) {// 需要依賴 mgo1 和 mgo2,所以不會執行
    mcs.Mgo1
    mcs.Mgo2
})

八、組

除了給依賴命名,還可以給依賴分組,相同類型的依賴會放入同一個切片中。不過分組後就不能通過命名的方式訪問依賴了,也就是說命名和組同時只能採用一種方式。還是上面那個例子。

使用參數

type MongodbClient struct {}

func NewMongoClient(cfg *Mongo.Client) *MongodbClient{
    return ConnectionMongo(cfg)
}

container.Provide(NewMongoClient, dig.Group("mgo"))
container.Provide(NewMongoClient, dig.Group("mgo"))

使用結果對象

type MongodbClientGroup struct {
    dig.In

    Mgos []*MongodbClient `group:"mgo"`
}

使用組

組只能通過對象參數的方式使用。

container.Invoke(func(mcg MongodbClientGroup) {
    for _, m := range mcg {
        // 業務邏輯
    }
})

注意事項

和命名依賴相似,嵌套 dig.In 的結構體所有帶有 gruop 標籤的字段必須都在 container 中至少存在一個,否則 Invoke 中傳入的函數不會被調用。除此之外,group 返回的切片不保證注入時的依賴順序,也就是說依賴切片是無序的。

九、使用組和命名的方式啓動多個 http 服務

到現在已經學完了 dig 所有的 API,下面稍微實戰一下,通過 dig 的依賴組啓動多個服務。

package main

import (
    "errors"
    "fmt"
    "net/http"
    "strconv"

    "go.uber.org/dig"
)

type ServerConfig struct {
    Host string // 主機地址
    Port string // 端口號
    Used bool   // 是否被佔用
}

type ServerGroup struct {
    dig.In

    Servers []*Server `group:"server"`
}

type ServerConfigNamed struct {
    dig.In

    Config1 *ServerConfig `name:"config1"`
    Config2 *ServerConfig `name:"config2"`
    Config3 *ServerConfig `name:"config3"`
}

type Server struct {
    Config *ServerConfig
}

func (s *Server) Run(i int) {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(write http.ResponseWriter, req *http.Request) {
        write.Write([]byte(fmt.Sprintf("第%s個服務,端口號是: %s", strconv.FormatInt(int64(i), 10), s.Config.Port)))
    })
    http.ListenAndServe(s.Config.Host+":"+s.Config.Port, mux)
}

func NewServerConfig(port string) func() *ServerConfig {
    return func() *ServerConfig {
        return &ServerConfig{Host: "127.0.0.1", Port: port, Used: false}
    }
}

func NewServer(sc ServerConfigNamed) *Server {
    if !sc.Config1.Used {
        sc.Config1.Used = true
        return &Server{Config: sc.Config1}
    } else if !sc.Config2.Used {
        sc.Config2.Used = true
        return &Server{Config: sc.Config2}
    } else if !sc.Config3.Used {
        sc.Config3.Used = true
        return &Server{Config: sc.Config3}
    }
    panic(errors.New(""))
}

func ServerRun(sg ServerGroup) {
    for i, s := range sg.Servers {
        go s.Run(i)
    }
}

func main() {
    container := dig.New()
    // 注入 3 個服務配置項
    container.Provide(NewServerConfig("8199"), dig.Name("config1"))
    container.Provide(NewServerConfig("8299"), dig.Name("config2"))
    container.Provide(NewServerConfig("8399"), dig.Name("config3"))
    // 注入 3 個服務實例
    container.Provide(NewServer, dig.Group("server"))
    container.Provide(NewServer, dig.Group("server"))
    container.Provide(NewServer, dig.Group("server"))
    // 使用緩衝 channel 卡住主協程
    serverChan := make(chan int, 1)
    container.Invoke(ServerRun)
    <-serverChan
}

運行該文件,可以通過訪問 http://127.0.0.1:8199、http://127.0.0.1:829、http://127.0.0.1:8399 查看效果。上面的示例使用命名依賴並不合適,但是爲了演示 API 的實際使用,所以使用了命名依賴。沒有哪種 API 是最好的。在實際開發中,根據具體業務,使用最適合場景的 API,靈活運用即可。

轉自:

juejin.cn/post/6898514836100120590

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Do-kTTbyKT4rsAGD3ujKwQ