Golang 依赖注入:dig

2022年3月26日 273点热度 0人点赞 0条评论

【导读】本文介绍依赖注入库 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 int1)
    container.Invoke(ServerRun)
    <-serverChan
}

运行该文件,可以通过访问 http://127.0.0.1:8199http://127.0.0.1:829http://127.0.0.1:8399 查看效果。上面的示例使用命名依赖并不合适,但是为了演示 API 的实际使用,所以使用了命名依赖。没有哪种 API 是最好的。在实际开发中,根据具体业务,使用最适合场景的 API,灵活运用即可。

转自:

juejin.cn/post/6898514836100120590

 - EOF -

推荐阅读(点击标题可打开)

1、Go module 使用 Gitlab 私有仓库

2、Golang 实现延迟消息原理与方法

3、Golang 的 协程调度机制 与 GOMAXPROCS 性能调优

Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

图片

关注后获取

回复 Go 获取6万star的Go资源库

分享、点赞和在看

支持我们分享更多好文章,谢谢!

45360Golang 依赖注入:dig

这个人很懒,什么都没留下

文章评论