简介

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

快速使用

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

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

下面看看如何使用:

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/jessevdk/go-flags"
  5. "go.uber.org/dig"
  6. "gopkg.in/ini.v1"
  7. )
  8. type Option struct {
  9. ConfigFile string `short:"c" long:"config" description:"Name of config file."`
  10. }
  11. func InitOption() (*Option, error) {
  12. var opt Option
  13. _, err := flags.Parse(&opt)
  14. return &opt, err
  15. }
  16. func InitConf(opt *Option) (*ini.File, error) {
  17. cfg, err := ini.Load(opt.ConfigFile)
  18. return cfg, err
  19. }
  20. func PrintInfo(cfg *ini.File) {
  21. fmt.Println("App Name:", cfg.Section("").Key("app_name").String())
  22. fmt.Println("Log Level:", cfg.Section("").Key("log_level").String())
  23. }
  24. func main() {
  25. container := dig.New()
  26. container.Provide(InitOption)
  27. container.Provide(InitConf)
  28. container.Invoke(PrintInfo)
  29. }

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

  1. app_name = awesome web
  2. log_level = DEBUG
  3. [mysql]
  4. ip = 127.0.0.1
  5. port = 3306
  6. user = dj
  7. password = 123456
  8. database = awesome
  9. [redis]
  10. ip = 127.0.0.1
  11. port = 6381

运行程序,输出:

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

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

  • 创建一个容器:dig.New
  • 为想要让dig容器管理的类型创建构造函数,构造函数可以返回多个值,这些值都会被容器管理;
  • 使用这些类型的时候直接编写一个函数,将这些类型作为参数,然后使用container.Invoke执行我们编写的函数。

参数对象

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

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

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

  1. type Params {
  2. dig.In
  3. Arg1 *Arg1
  4. Arg2 *Arg2
  5. Arg3 *Arg3
  6. Arg4 *Arg4
  7. }
  8. container.Provide(func (params Params) *Object {
  9. // ...
  10. })

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

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/jessevdk/go-flags"
  6. "go.uber.org/dig"
  7. "gopkg.in/ini.v1"
  8. )
  9. type Option struct {
  10. ConfigFile string `short:"c" long:"config" description:"Name of config file."`
  11. }
  12. type RedisConfig struct {
  13. IP string
  14. Port int
  15. DB int
  16. }
  17. type MySQLConfig struct {
  18. IP string
  19. Port int
  20. User string
  21. Password string
  22. Database string
  23. }
  24. type Config struct {
  25. dig.In
  26. Redis *RedisConfig
  27. MySQL *MySQLConfig
  28. }
  29. func InitOption() (*Option, error) {
  30. var opt Option
  31. _, err := flags.Parse(&opt)
  32. return &opt, err
  33. }
  34. func InitConfig(opt *Option) (*ini.File, error) {
  35. cfg, err := ini.Load(opt.ConfigFile)
  36. return cfg, err
  37. }
  38. func InitRedisConfig(cfg *ini.File) (*RedisConfig, error) {
  39. port, err := cfg.Section("redis").Key("port").Int()
  40. if err != nil {
  41. log.Fatal(err)
  42. return nil, err
  43. }
  44. db, err := cfg.Section("redis").Key("db").Int()
  45. if err != nil {
  46. log.Fatal(err)
  47. return nil, err
  48. }
  49. return &RedisConfig{
  50. IP: cfg.Section("redis").Key("ip").String(),
  51. Port: port,
  52. DB: db,
  53. }, nil
  54. }
  55. func InitMySQLConfig(cfg *ini.File) (*MySQLConfig, error) {
  56. port, err := cfg.Section("mysql").Key("port").Int()
  57. if err != nil {
  58. return nil, err
  59. }
  60. return &MySQLConfig{
  61. IP: cfg.Section("mysql").Key("ip").String(),
  62. Port: port,
  63. User: cfg.Section("mysql").Key("user").String(),
  64. Password: cfg.Section("mysql").Key("password").String(),
  65. Database: cfg.Section("mysql").Key("database").String(),
  66. }, nil
  67. }
  68. func PrintInfo(config Config) {
  69. fmt.Println("=========== redis section ===========")
  70. fmt.Println("redis ip:", config.Redis.IP)
  71. fmt.Println("redis port:", config.Redis.Port)
  72. fmt.Println("redis db:", config.Redis.DB)
  73. fmt.Println("=========== mysql section ===========")
  74. fmt.Println("mysql ip:", config.MySQL.IP)
  75. fmt.Println("mysql port:", config.MySQL.Port)
  76. fmt.Println("mysql user:", config.MySQL.User)
  77. fmt.Println("mysql password:", config.MySQL.Password)
  78. fmt.Println("mysql db:", config.MySQL.Database)
  79. }
  80. func main() {
  81. container := dig.New()
  82. container.Provide(InitOption)
  83. container.Provide(InitConfig)
  84. container.Provide(InitRedisConfig)
  85. container.Provide(InitMySQLConfig)
  86. err := container.Invoke(PrintInfo)
  87. if err != nil {
  88. log.Fatal(err)
  89. }
  90. }

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

运行结果:

  1. $ go run main.go -c=my.ini
  2. =========== redis section ===========
  3. redis ip: 127.0.0.1
  4. redis port: 6381
  5. redis db: 1
  6. =========== mysql section ===========
  7. mysql ip: 127.0.0.1
  8. mysql port: 3306
  9. mysql user: dj
  10. mysql password: 123456
  11. mysql db: awesome

结果对象

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

  1. type Results struct {
  2. dig.Out
  3. Result1 *Result1
  4. Result2 *Result2
  5. Result3 *Result3
  6. Result4 *Result4
  7. }
  1. dig.Provide(func () (Results, error) {
  2. // ...
  3. })

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

  1. type Config struct {
  2. dig.Out
  3. Redis *RedisConfig
  4. MySQL *MySQLConfig
  5. }

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

  1. func InitRedisAndMySQLConfig(cfg *ini.File) (Config, error) {
  2. var config Config
  3. redis, err := InitRedisConfig(cfg)
  4. if err != nil {
  5. return config, err
  6. }
  7. mysql, err := InitMySQLConfig(cfg)
  8. if err != nil {
  9. return config, err
  10. }
  11. config.Redis = redis
  12. config.MySQL = mysql
  13. return config, nil
  14. }
  15. func main() {
  16. container := dig.New()
  17. container.Provide(InitOption)
  18. container.Provide(InitConfig)
  19. container.Provide(InitRedisAndMySQLConfig)
  20. err := container.Invoke(PrintInfo)
  21. if err != nil {
  22. log.Fatal(err)
  23. }
  24. }

PrintInfo直接依赖RedisConfigMySQLConfig

  1. func PrintInfo(redis *RedisConfig, mysql *MySQLConfig) {
  2. fmt.Println("=========== redis section ===========")
  3. fmt.Println("redis ip:", redis.IP)
  4. fmt.Println("redis port:", redis.Port)
  5. fmt.Println("redis db:", redis.DB)
  6. fmt.Println("=========== mysql section ===========")
  7. fmt.Println("mysql ip:", mysql.IP)
  8. fmt.Println("mysql port:", mysql.Port)
  9. fmt.Println("mysql user:", mysql.User)
  10. fmt.Println("mysql password:", mysql.Password)
  11. fmt.Println("mysql db:", mysql.Database)
  12. }

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

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

可选依赖

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

  1. type Config struct {
  2. dig.In
  3. Redis *RedisConfig `optional:"true"`
  4. MySQL *MySQLConfig
  5. }

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

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

  1. // 省略部分代码
  2. func PrintInfo(config Config) {
  3. if config.Redis == nil {
  4. fmt.Println("no redis config")
  5. }
  6. }
  7. func main() {
  8. container := dig.New()
  9. container.Provide(InitOption)
  10. container.Provide(InitConfig)
  11. container.Provide(InitMySQLConfig)
  12. container.Invoke(PrintInfo)
  13. }

输出:

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

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

命名

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

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

  1. type User struct {
  2. Name string
  3. Age int
  4. }
  5. func NewUser(name string, age int) func() *User{} {
  6. return func() *User {
  7. return &User{name, age}
  8. }
  9. }
  10. container.Provide(NewUser("dj", 18), dig.Name("dj"))
  11. container.Provide(NewUser("dj2", 18), dig.Name("dj2"))

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

  1. type UserResults struct {
  2. dig.Out
  3. User1 *User `name:"dj"`
  4. User2 *User `name:"dj2"`
  5. }

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

  1. type UserParams struct {
  2. dig.In
  3. User1 *User `name:"dj"`
  4. User2 *User `name:"dj2"`
  5. }

完整代码:

  1. package main
  2. import (
  3. "fmt"
  4. "go.uber.org/dig"
  5. )
  6. type User struct {
  7. Name string
  8. Age int
  9. }
  10. func NewUser(name string, age int) func() *User {
  11. return func() *User {
  12. return &User{name, age}
  13. }
  14. }
  15. type UserParams struct {
  16. dig.In
  17. User1 *User `name:"dj"`
  18. User2 *User `name:"dj2"`
  19. }
  20. func PrintInfo(params UserParams) error {
  21. fmt.Println("User 1 ===========")
  22. fmt.Println("Name:", params.User1.Name)
  23. fmt.Println("Age:", params.User1.Age)
  24. fmt.Println("User 2 ===========")
  25. fmt.Println("Name:", params.User2.Name)
  26. fmt.Println("Age:", params.User2.Age)
  27. return nil
  28. }
  29. func main() {
  30. container := dig.New()
  31. container.Provide(NewUser("dj", 18), dig.Name("dj"))
  32. container.Provide(NewUser("dj2", 18), dig.Name("dj2"))
  33. container.Invoke(PrintInfo)
  34. }

程序运行结果:

  1. $ go run main.go
  2. User 1 ===========
  3. Name: dj
  4. Age: 18
  5. User 2 ===========
  6. Name: dj2
  7. Age: 18

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

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

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

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

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

  1. type UserParams struct {
  2. dig.In
  3. Users []User `group:"user"`
  4. }
  5. func Info(params UserParams) error {
  6. for _, u := range params.Users {
  7. fmt.Println(u.Name, u.Age)
  8. }
  9. return nil
  10. }
  11. container.Invoke(Info)

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

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "go.uber.org/dig"
  6. )
  7. type Handler struct {
  8. Greeting string
  9. Path string
  10. }
  11. func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  12. fmt.Fprintf(w, "%s from %s", h.Greeting, h.Path)
  13. }
  14. func NewHello1Handler() HandlerResult {
  15. return HandlerResult{
  16. Handler: Handler{
  17. Path: "/hello1",
  18. Greeting: "welcome",
  19. },
  20. }
  21. }
  22. func NewHello2Handler() HandlerResult {
  23. return HandlerResult{
  24. Handler: Handler{
  25. Path: "/hello2",
  26. Greeting: "😄",
  27. },
  28. }
  29. }
  30. type HandlerResult struct {
  31. dig.Out
  32. Handler Handler `group:"server"`
  33. }
  34. type HandlerParams struct {
  35. dig.In
  36. Handlers []Handler `group:"server"`
  37. }
  38. func RunServer(params HandlerParams) error {
  39. mux := http.NewServeMux()
  40. for _, h := range params.Handlers {
  41. mux.Handle(h.Path, h)
  42. }
  43. server := &http.Server{
  44. Addr: ":8080",
  45. Handler: mux,
  46. }
  47. if err := server.ListenAndServe(); err != nil {
  48. return err
  49. }
  50. return nil
  51. }
  52. func main() {
  53. container := dig.New()
  54. container.Provide(NewHello1Handler)
  55. container.Provide(NewHello2Handler)
  56. container.Invoke(RunServer)
  57. }

我们创建了两个处理器,添加到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😄

参考

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