Go 每日一庫之 dig

簡介

今天咱們來介紹 Go 語言的一個依賴注入(DI)庫——dig。dig 是 uber 開源的庫。Java 依賴注入的庫有不少,相信即便不是作 Java 開發的童鞋也聽過大名鼎鼎的 Spring。相比龐大的 Spring,dig 很小巧,實現和使用都比較簡潔。mysql

快速使用

第三方庫須要先安裝,因爲咱們的示例中使用了前面介紹的go-inigo-flags,這兩個庫也須要安裝:git

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

下面看看如何使用:github

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.inigolang

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

運行程序,輸出:web

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

dig庫幫助開發者管理這些對象的建立和維護,每種類型的對象會建立且只建立一次dig庫使用的通常流程:redis

  • 建立一個容器:dig.New
  • 爲想要讓dig容器管理的類型建立構造函數,構造函數能夠返回多個值,這些值都會被容器管理;
  • 使用這些類型的時候直接編寫一個函數,將這些類型做爲參數,而後使用container.Invoke執行咱們編寫的函數。

參數對象

有時候,建立對象有不少依賴,或者編寫函數時有多個參數依賴。若是將這些依賴都做爲參數傳入,那麼代碼將變得很是難以閱讀:sql

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對象傳給PrintInfosegmentfault

運行結果:

$ 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: "😄",
    },
  }
}

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😄

參考

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

個人博客

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索