Go 每日一库之 dig

栏目: IT技术 · 发布时间: 4年前

内容简介:今天我们来介绍 Go 语言的一个依赖注入(DI)库——第三方库需要先安装,由于我们的示例中使用了前面介绍的下面看看如何使用:

简介

今天我们来介绍 Go 语言的一个依赖注入(DI)库—— dig 。dig 是 uber 开源的库。Java 依赖注入的库有很多,相信即使不是做 Java 开发的童鞋也听过大名鼎鼎的 Spring。相比庞大的 Spring,dig 很小巧,实现和使用都比较简洁。

快速使用

第三方库需要先安装,由于我们的示例中使用了前面介绍的 go-inigo-flags ,这两个库也需要安装:

$ go get go.uber.org/dig
$ go get gopkg.in/ini.v1
$ go get github.com/jessevdk/go-flags

下面看看如何使用:

package main

import (
  "fmt"

  "github.com/jessevdk/go-flags"
  "go.uber.org/dig"
  "gopkg.in/ini.v1"
)

type Option struct {
  ConfigFile string `short:"c" long:"config" description:"Name of config file."`
}

func InitOption() (*Option, error) {
  var opt Option
  _, err := flags.Parse(&opt)

  return &opt, err
}

func InitConf(opt *Option) (*ini.File, error) {
  cfg, err := ini.Load(opt.ConfigFile)
  return cfg, err
}

func PrintInfo(cfg *ini.File) {
  fmt.Println("App Name:", cfg.Section("").Key("app_name").String())
  fmt.Println("Log Level:", cfg.Section("").Key("log_level").String())
}

func main() {
  container := dig.New()

  container.Provide(InitOption)
  container.Provide(InitConf)

  container.Invoke(PrintInfo)
}

在同一目录下创建配置文件 my.ini

app_name = awesome web
log_level = DEBUG

[mysql]
ip = 127.0.0.1
port = 3306
user = dj
password = 123456
database = awesome

[redis]
ip = 127.0.0.1
port = 6381

运行程序,输出:

$ go run main.go -c=my.ini
App Name: awesome web
Log Level: DEBUG

dig 库帮助开发者管理这些对象的创建和维护,每种类型的对象会 创建且只创建一次dig 库使用的一般流程:

dig.New
dig
container.Invoke

参数对象

有时候,创建对象有很多依赖,或者编写函数时有多个参数依赖。如果将这些依赖都作为参数传入,那么代码将变得非常难以阅读:

container.Provide(func (arg1 *Arg1, arg2 *Arg2, arg3 *Arg3, ....) {
  // ...
})

dig 支持将所有参数打包进一个对象中,唯一需要的就是将 dig.In 内嵌到该类型中:

type Params {
  dig.In

  Arg1 *Arg1
  Arg2 *Arg2
  Arg3 *Arg3
  Arg4 *Arg4
}

container.Provide(func (params Params) *Object {
  // ...
})

内嵌了 dig.In 之后, dig 会将该类型中的其它字段看成 Object 的依赖,创建 Object 类型的对象时,会先将依赖的 Arg1/Arg2/Arg3/Arg4 创建好。

package main

import (
  "fmt"
  "log"

  "github.com/jessevdk/go-flags"
  "go.uber.org/dig"
  "gopkg.in/ini.v1"
)

type Option struct {
  ConfigFile string `short:"c" long:"config" description:"Name of config file."`
}

type RedisConfig struct {
  IP   string
  Port int
  DB   int
}

type MySQLConfig struct {
  IP       string
  Port     int
  User     string
  Password string
  Database string
}

type Config struct {
  dig.In

  Redis *RedisConfig
  MySQL *MySQLConfig
}

func InitOption() (*Option, error) {
  var opt Option
  _, err := flags.Parse(&opt)

  return &opt, err
}

func InitConfig(opt *Option) (*ini.File, error) {
  cfg, err := ini.Load(opt.ConfigFile)
  return cfg, err
}

func InitRedisConfig(cfg *ini.File) (*RedisConfig, error) {
  port, err := cfg.Section("redis").Key("port").Int()
  if err != nil {
    log.Fatal(err)
    return nil, err
  }

  db, err := cfg.Section("redis").Key("db").Int()
  if err != nil {
    log.Fatal(err)
    return nil, err
  }

  return &RedisConfig{
    IP:   cfg.Section("redis").Key("ip").String(),
    Port: port,
    DB:   db,
  }, nil
}

func InitMySQLConfig(cfg *ini.File) (*MySQLConfig, error) {
  port, err := cfg.Section("mysql").Key("port").Int()
  if err != nil {
    return nil, err
  }

  return &MySQLConfig{
    IP:       cfg.Section("mysql").Key("ip").String(),
    Port:     port,
    User:     cfg.Section("mysql").Key("user").String(),
    Password: cfg.Section("mysql").Key("password").String(),
    Database: cfg.Section("mysql").Key("database").String(),
  }, nil
}

func PrintInfo(config Config) {
  fmt.Println("=========== redis section ===========")
  fmt.Println("redis ip:", config.Redis.IP)
  fmt.Println("redis port:", config.Redis.Port)
  fmt.Println("redis db:", config.Redis.DB)

  fmt.Println("=========== mysql section ===========")
  fmt.Println("mysql ip:", config.MySQL.IP)
  fmt.Println("mysql port:", config.MySQL.Port)
  fmt.Println("mysql user:", config.MySQL.User)
  fmt.Println("mysql password:", config.MySQL.Password)
  fmt.Println("mysql db:", config.MySQL.Database)
}

func main() {
  container := dig.New()

  container.Provide(InitOption)
  container.Provide(InitConfig)
  container.Provide(InitRedisConfig)
  container.Provide(InitMySQLConfig)

  err := container.Invoke(PrintInfo)
  if err != nil {
    log.Fatal(err)
  }
}

上面代码中,类型 Config 内嵌了 dig.InPrintInfo 接受一个 Config 类型的参数。调用 Invoke 时, dig 自动调用 InitRedisConfigInitMySQLConfig ,并将生成的 *RedisConfig*MySQLConfig “打包”成一个 Config 对象传给 PrintInfo

运行结果:

$ go run main.go -c=my.ini
=========== redis section ===========
redis ip: 127.0.0.1
redis port: 6381
redis db: 1
=========== mysql section ===========
mysql ip: 127.0.0.1
mysql port: 3306
mysql user: dj
mysql password: 123456
mysql db: awesome

结果对象

前面说过,如果构造函数返回多个值,这些不同类型的值都会存储到 dig 容器中。参数过多会影响代码的可读性和可维护性,返回值过多同样也是如此。为此, dig 提供了返回值对象,返回一个包含多个类型对象的对象。返回的类型,必须内嵌 dig.Out

type Results struct {
  dig.Out

  Result1 *Result1
  Result2 *Result2
  Result3 *Result3
  Result4 *Result4
}
dig.Provide(func () (Results, error) {
  // ...
})

我们把上面的例子稍作修改。将 Config 内嵌的 dig.In 变为 dig.Out

type Config struct {
  dig.Out

  Redis *RedisConfig
  MySQL *MySQLConfig
}

提供构造函数 InitRedisAndMySQLConfig 同时创建 RedisConfigMySQLConfig ,通过 Config 返回。这样就不需要将 InitRedisConfigInitMySQLConfig 加入 dig 容器了:

func InitRedisAndMySQLConfig(cfg *ini.File) (Config, error) {
  var config Config

  redis, err := InitRedisConfig(cfg)
  if err != nil {
    return config, err
  }

  mysql, err := InitMySQLConfig(cfg)
  if err != nil {
    return config, err
  }

  config.Redis = redis
  config.MySQL = mysql
  return config, nil
}

func main() {
  container := dig.New()

  container.Provide(InitOption)
  container.Provide(InitConfig)
  container.Provide(InitRedisAndMySQLConfig)

  err := container.Invoke(PrintInfo)
  if err != nil {
    log.Fatal(err)
  }
}

PrintInfo 直接依赖 RedisConfigMySQLConfig

func PrintInfo(redis *RedisConfig, mysql *MySQLConfig) {
  fmt.Println("=========== redis section ===========")
  fmt.Println("redis ip:", redis.IP)
  fmt.Println("redis port:", redis.Port)
  fmt.Println("redis db:", redis.DB)

  fmt.Println("=========== mysql section ===========")
  fmt.Println("mysql ip:", mysql.IP)
  fmt.Println("mysql port:", mysql.Port)
  fmt.Println("mysql user:", mysql.User)
  fmt.Println("mysql password:", mysql.Password)
  fmt.Println("mysql db:", mysql.Database)
}

可以看到 InitRedisAndMySQLConfig 返回 Config 类型的对象,该类型中的 RedisConfigMySQLConfig 都被添加到了容器中, PrintInfo 函数可直接使用。

运行结果与之前的例子完全一样。

可选依赖

默认情况下,容器如果找不到对应的依赖,那么相应的对象无法创建成功,调用 Invoke 时也会返回错误。有些依赖不是必须的, dig 也提供了一种方式将依赖设置为可选的:

type Config struct {
  dig.In

  Redis *RedisConfig `optional:"true"`
  MySQL *MySQLConfig
}

通过在字段后添加结构标签 optional:"true" ,我们将 RedisConfig 这个依赖设置为可选的,容器中 RedisConfig 对象也不要紧,这时传入的 Configredis 为 nil,方法可以正常调用。显然可选依赖只能在参数对象中使用。

我们直接注释掉 InitRedisConfig ,然后运行程序:

// 省略部分代码
func PrintInfo(config Config) {
  if config.Redis == nil {
    fmt.Println("no redis config")
  }
}

func main() {
  container := dig.New()

  container.Provide(InitOption)
  container.Provide(InitConfig)
  container.Provide(InitMySQLConfig)

  container.Invoke(PrintInfo)
}

输出:

$ go run main.go -c=my.ini
no redis config

注意,创建失败和没有提供构造函数是两个概念。如果 InitRedisConfig 调用失败了,使用 Invoke 执行 PrintInfo 还是会报错的。

命名

前面我们说过, dig 默认只会为每种类型创建一个对象。如果要创建某个类型的多个对象怎么办呢?可以为对象命名!

调用容器的 Provide 方法时,可以为构造函数的返回对象命名,这样同一个类型就可以有多个对象了。

type User struct {
  Name string
  Age  int
}

func NewUser(name string, age int) func() *User{} {
  return func() *User {
    return &User{name, age}
  }
}
container.Provide(NewUser("dj", 18), dig.Name("dj"))
container.Provide(NewUser("dj2", 18), dig.Name("dj2"))

也可以在结果对象中通过结构标签指定:

type UserResults struct {
  dig.Out

  User1 *User `name:"dj"`
  User2 *User `name:"dj2"`
}

然后在参数对象中通过名字指定使用哪个对象:

type UserParams struct {
  dig.In

  User1 *User `name:"dj"`
  User2 *User `name:"dj2"`
}

完整代码:

package main

import (
  "fmt"

  "go.uber.org/dig"
)

type User struct {
  Name string
  Age  int
}

func NewUser(name string, age int) func() *User {
  return func() *User {
    return &User{name, age}
  }
}

type UserParams struct {
  dig.In

  User1 *User `name:"dj"`
  User2 *User `name:"dj2"`
}

func PrintInfo(params UserParams) error {
  fmt.Println("User 1 ===========")
  fmt.Println("Name:", params.User1.Name)
  fmt.Println("Age:", params.User1.Age)

  fmt.Println("User 2 ===========")
  fmt.Println("Name:", params.User2.Name)
  fmt.Println("Age:", params.User2.Age)
  return nil
}

func main() {
  container := dig.New()

  container.Provide(NewUser("dj", 18), dig.Name("dj"))
  container.Provide(NewUser("dj2", 18), dig.Name("dj2"))

  container.Invoke(PrintInfo)
}

程序运行结果:

$ go run main.go
User 1 ===========
Name: dj
Age: 18
User 2 ===========
Name: dj2
Age: 18

需要注意的时候, NewUser 返回的是一个函数,由 dig 在需要的时候调用。

组可以将相同类型的对象放到一个切片中,可以直接使用这个切片。组的定义与上面名字定义类似。可以通过为 Provide 提供额外的参数:

container.Provide(NewUser("dj", 18), dig.Group("user"))
container.Provide(NewUser("dj2", 18), dig.Group("user"))

也可以在结果对象中添加结构标签 group:"user"

然后我们定义一个参数对象,通过指定同样的结构标签来使用这个切片:

type UserParams struct {
  dig.In

  Users []User `group:"user"`
}

func Info(params UserParams) error {
  for _, u := range params.Users {
    fmt.Println(u.Name, u.Age)
  }

  return nil
}

container.Invoke(Info)

最后我们通过一个完整的例子演示组的使用,我们将创建一个 HTTP 服务器:

package main

import (
  "fmt"
  "net/http"

  "go.uber.org/dig"
)

type Handler struct {
  Greeting string
  Path     string
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "%s from %s", h.Greeting, h.Path)
}

func NewHello1Handler() HandlerResult {
  return HandlerResult{
    Handler: Handler{
      Path:     "/hello1",
      Greeting: "welcome",
    },
  }
}

func NewHello2Handler() HandlerResult {
  return HandlerResult{
    Handler: Handler{
      Path:     "/hello2",
      Greeting: ":smile:",
    },
  }
}

type HandlerResult struct {
  dig.Out

  Handler Handler `group:"server"`
}

type HandlerParams struct {
  dig.In

  Handlers []Handler `group:"server"`
}

func RunServer(params HandlerParams) error {
  mux := http.NewServeMux()
  for _, h := range params.Handlers {
    mux.Handle(h.Path, h)
  }

  server := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }
  if err := server.ListenAndServe(); err != nil {
    return err
  }

  return nil
}

func main() {
  container := dig.New()

  container.Provide(NewHello1Handler)
  container.Provide(NewHello2Handler)

  container.Invoke(RunServer)
}

我们创建了两个处理器,添加到 server 组中,在 RunServer 函数中创建 HTTP 服务器,将这些处理器注册到服务器中。

运行程序,在浏览器中输入 localhost:8080/hello1localhost:8080/hello2 看看。关于 Go Web 编程相关的知识,可以看看我写的 Go Web 编程系列文章:

常见错误

使用 dig 过程中会遇到一些错误,我们来看看常见的错误。

Invoke 方法在以下几种情况下会返回一个 error

  • 无法找到依赖,或依赖创建失败;
  • Invoke 执行的函数返回 error ,该错误也会被传给调用者。

这两种情况,我们都可以判断 Invoke 的返回值来查找原因。

总结

本文介绍了 dig 库,它适用于解决循环依赖的对象创建问题。同时也有利于将关注点分离,我们不需要将各种对象传来传去,只需要将构造函数交给 dig 容器,然后通过 Invoke 直接使用依赖即可,连判空逻辑都可以省略了!

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue:smile:

参考

  1. dig GitHub: https://github.com/uber-go/dig
  2. Go 每日一库 GitHub: https://github.com/darjun/go-daily-lib

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

Go 每日一库之 dig

本文由博客一文多发平台 OpenWrite 发布!


以上所述就是小编给大家介绍的《Go 每日一库之 dig》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

社群营销与运营/互联网+新媒体营销规划丛书

社群营销与运营/互联网+新媒体营销规划丛书

秦阳//秋叶|总主编:秋叶 / 人民邮电出版社 / 2017-5 / 45.00元

《社群营销与运营》共分6章。第1章重点介绍了社群营销的起因、概念、构成、价值和评估模型,引导读者全面认识社群以及社群营销;第2章介绍了如何从无到有、从小到大建设一个社群的手法和注意事项;第3章重点介绍维持社群活跃度的各种技巧;第4章介绍了组织一场社群线下活动五个阶段的执行方案;第5章介绍了如何从无到有、由弱到强地构建社群运营团队;第6章介绍如何正确看待社群商业变现以及社群商业变现的三大模式和四个基......一起来看看 《社群营销与运营/互联网+新媒体营销规划丛书》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具