分解uber依赖注入库dig-使用篇

golang的依赖注入库非常的少,好用的更是少之又少,比较好用的目前有两个

谷歌出的wire,这个是用抽象语法树在编译时实现的。
uber出的dig,在运行时,用返射实现的,并基于dig库,写了一个依赖框架fx
本系列分几部分,先对dig进行分析,第一篇介绍dig的使用,第二篇再从源码来剖析他是如何通过返射实现的的依赖注入的,后续会介绍fx 的使用和实现原理。
dig主要的思路是能过Provider将不同的函数注册到容器内,一个函数可以通过参数来声明对其他函数返回值的依赖。在Invoke的时候才会真正的去调用容器内相应的Provider方法。
dig还提供了可视化的方法Visualize用于生成dot有向图代码,更直观的观察依赖关系,关于dot的基本语法,可以查看帖子dot 语法总结
帖子中的代码使用的dig的master分支,版本为1.11.0-dev,帖子所有的代码都在github上,地址:fx_dig_adventure

简单使用
func TestSimple1(t *testing.T) {
type Config struct {
Prefix string
}

c := dig.New()

err := c.Provide(func() (*Config, error) {
	return &Config{Prefix: "[foo] "}, nil
})
if err != nil {
	panic(err)
}
err = c.Provide(func(cfg *Config) *log.Logger {
	return log.New(os.Stdout, cfg.Prefix, 0)
})
if err != nil {
	panic(err)
}
err = c.Invoke(func(l *log.Logger) {
	l.Print("You've been invoked")
})
if err != nil {
	panic(err)
}

}
输出

[foo] You’ve been invoked
可以生成dot图,来更直观的查看依赖关系

b := &bytes.Buffer{}
if err := dig.Visualize(c, b); err != nil {
	panic(err)
}
fmt.Println(b.String())

输出

digraph {
rankdir=RL;
graph [compound=true];
subgraph cluster_0 {
label = “main”;
constructor_0 [shape=plaintext label=“main.func1”];

                    "*main.Config" [label=<*main.Config>];
                    
            }
            subgraph cluster_1 {
                    label = "main";
                    constructor_1 [shape=plaintext label="main.func2"];
                    
                    "*log.Logger" [label=<*log.Logger>];
                    
            }
            constructor_1 -> "*main.Config" [ltail=cluster_1];

}

可以看到 func2返回的参数为Log 依赖 func1返回参数 Config。dot 语法总结
展示出来:

命名参数–多个返回相同类型的Provide
如果Provide里提供的函数,有多个函数返回的数据类型是一样的怎么处理?比如,我们的数据库有主从两个连接库,怎么进行区分?
dig可以将Provide命名以进行区分
我们可以直接在Provide函数里使用dig.Name,为相同的返回类型设置不同的名字来进行区分。

func TestName1(t *testing.T) {
type DSN struct {
Addr string
}
c := dig.New()

p1 := func() (*DSN, error) {
	return &DSN{Addr: "primary DSN"}, nil
}
if err := c.Provide(p1, dig.Name("primary")); err != nil {
	t.Fatal(err)
}

p2 := func() (*DSN, error) {
	return &DSN{Addr: "secondary DSN"}, nil
}
if err := c.Provide(p2, dig.Name("secondary")); err != nil {
	t.Fatal(err)
}

type DBInfo struct {
	dig.In
	PrimaryDSN   *DSN `name:"primary"`
	SecondaryDSN *DSN `name:"secondary"`
}

if err := c.Invoke(func(db DBInfo) {
	t.Log(db.PrimaryDSN)
	t.Log(db.SecondaryDSN)
}); err != nil {
	t.Fatal(err)
}

}
输出

&{primary DSN}
&{secondary DSN}
dot图

这样做并不通用,一般我们是有一个结构体来实现,dig也有相应的支持,用一个结构体嵌入dig.out来实现,
相同类型的字段在tag里设置不同的name来实现

func TestName2(t *testing.T) {
type DSN struct {
Addr string
}
c := dig.New()

type DSNRev struct {
	dig.Out
	PrimaryDSN   *DSN `name:"primary"`
	SecondaryDSN *DSN `name:"secondary"`
}
p1 := func() (DSNRev, error) {
	return DSNRev{PrimaryDSN: &DSN{Addr: "Primary DSN"},
		SecondaryDSN: &DSN{Addr: "Secondary DSN"}}, nil
}

if err := c.Provide(p1); err != nil {
	t.Fatal(err)
}

type DBInfo struct {
	dig.In
	PrimaryDSN   *DSN `name:"primary"`
	SecondaryDSN *DSN `name:"secondary"`
}
inv1 := func(db DBInfo) {
	t.Log(db.PrimaryDSN)
	t.Log(db.SecondaryDSN)
}

if err := c.Invoke(inv1); err != nil {
	t.Fatal(err)
}

}

输出

&{primary DSN}
&{secondary DSN}
dot 图

和上面的不同之处就是一个function返回了两个相同类型的字段。

组–把同类型的参数放在一个slice里
如果有很多相同类型的返回参数,可以把他们放在同一个slice里,和命名方式一样,有两种使用方式
第一种在调用Provide时直接使用dig.Group

func TestGroup1(t *testing.T) {
type Student struct {
Name string
Age int
}
NewUser := func(name string, age int) func() *Student {
return func() *Student {
return &Student{name, age}
}
}
container := dig.New()
if err := container.Provide(NewUser(“tom”, 3), dig.Group(“stu”)); err != nil {
t.Fatal(err)
}
if err := container.Provide(NewUser(“jerry”, 1), dig.Group(“stu”)); err != nil {
t.Fatal(err)
}
type inParams struct {
dig.In

	StudentList []*Student `group:"stu"`
}
Info := func(params inParams) error {
	for _, u := range params.StudentList {
		t.Log(u.Name, u.Age)
	}
	return nil
}
if err := container.Invoke(Info); err != nil {
	t.Fatal(err)
}

}
输出

jerry 1
tom 3
生成dot图

或者使用结构体嵌入dig.Out来实现,tag里要加上了group标签

type Rep struct {
	dig.Out
	StudentList []*Student `group:"stu,flatten"`
}

这个flatten的意思是,底层把组表示成[]*Student,如果不加flatten会表示成[][]*Student
完整示例

func TestGroup2(t *testing.T) {
type Student struct {
Name string
Age int
}
type Rep struct {
dig.Out
StudentList []*Student group:"stu,flatten"
}
NewUser := func(name string, age int) func() Rep {
return func() Rep {
r := Rep{}
r.StudentList = append(r.StudentList, &Student{
Name: name,
Age: age,
})
return r
}
}

container := dig.New()
if err := container.Provide(NewUser("tom", 3)); err != nil {
	t.Fatal(err)
}
if err := container.Provide(NewUser("jerry", 1)); err != nil {
	t.Fatal(err)
}
type InParams struct {
	dig.In

	StudentList []*Student `group:"stu"`
}
Info := func(params InParams) error {
	for _, u := range params.StudentList {
		t.Log(u.Name, u.Age)
	}
	return nil
}
if err := container.Invoke(Info); err != nil {
	t.Fatal(err)
}

}
输出

jerry 1
tom 3
生成dot图

从dot图可以看出有两个方法生成了Group: stu

需要注意的一点是,命名方式和组方式不能同时使用。

可选参数
如果注册的方法返回的参数是可以为nil的,可以使用option来实现

func TestOption1(t *testing.T) {
type Student struct {
dig.Out
Name string
Age *int option:"true"
}

c := dig.New()
if err := c.Provide(func() Student {
	return Student{
		Name: "Tom",
	}
}); err != nil {
	t.Fatal(err)
}

if err := c.Invoke(func(n string, age *int) {
	t.Logf("name: %s", n)
	if age == nil {
		t.Log("age is nil")
	} else {
		t.Logf("age: %d", age)
	}
}); err != nil {
	t.Fatal(err)
}

}
输出

name: Tom
age is nil
dry run
如果我们只是想看一下依赖注入的整个流程是不是通的,可以通过dry run来跑一下,他不会调用具体的函数,而是直接返回函数的返回参数的zero值

func TestDryRun1(t *testing.T) {
// Dry Run
c := dig.New(dig.DryRun(true))

type Config struct {
	Prefix string
}
err := c.Provide(func() (*Config, error) {
	return &Config{Prefix: "[foo] "}, nil
})
if err != nil {
	panic(err)
}
err = c.Provide(func(cfg *Config) *log.Logger {
	return log.New(os.Stdout, cfg.Prefix, 0)
})
if err != nil {
	panic(err)
}
err = c.Invoke(func(l *log.Logger) {
	l.Print("You've been invoked")
})
if err != nil {
	panic(err)
}

}
运行代码不会有任何输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值