简介

今天我们来介绍 Go 语言的一个依赖注入(DI)库——dig。

dig 是 uber 开源的库。

Java 依赖注入的库有很多,相信即使不是做 Java 开发的童鞋也听过大名鼎鼎的 Spring。

相比庞大的 Spring,dig 很小巧,实现和使用都比较简洁。

快速使用

第三方库需要先安装,由于我们的示例中使用了前面介绍的go-ini和go-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.Newdigcontainer.Invoke

参数对象

有时候,创建对象有很多依赖,或者编写函数时有多个参数依赖。

如果将这些依赖都作为参数传入,那么代码将变得非常难以阅读:

container.Provide(func (arg1 *Arg1, arg2 *Arg2, arg3 *Arg3, ....) {
	// ...
})
digdig.In
type Params {
	dig.In

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

container.Provide(func (params Params) *Object {
	// ...
})
dig.IndigObjectObjectArg1/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)
	}
}
Configdig.InPrintInfoConfigInvokedigInitRedisConfigInitMySQLConfig*RedisConfig*MySQLConfigConfigPrintInfo

运行结果:

$ 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

结果对象

digdigdig.Out
type Results struct {
	dig.Out

	Result1 *Result1
	Result2 *Result2
	Result3 *Result3
	Result4 *Result4
}
dig.Provide(func () (Results, error) {
	// ...
})
Configdig.Indig.Out
type Config struct {
	dig.Out

	Redis *RedisConfig
	MySQL *MySQLConfig
}
InitRedisAndMySQLConfigRedisConfigMySQLConfigConfig
InitRedisConfigInitMySQLConfigdig
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)
	}
}
PrintInfoRedisConfigMySQLConfig
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)
}
InitRedisAndMySQLConfigConfigRedisConfigMySQLConfigPrintInfo

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

可选依赖

Invokedig
type Config struct {
	dig.In

	Redis *RedisConfig `optional:"true"`
	MySQL *MySQLConfig
}
optional:"true"RedisConfigRedisConfigConfigredis
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

注意,创建失败和没有提供构造函数是两个概念。

InitRedisConfigInvokePrintInfo

命名

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
NewUserdig

组可以将相同类型的对象放到一个切片中,可以直接使用这个切片。组的定义与上面名字定义类似。

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)
}
serverRunServer
localhost:8080/hello1localhost:8080/hello2

常见错误

dig
Invokeerror
Invokeerror
Invoke

总结

digdigInvoke

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

参考