golang的依赖注入库非常的少,好用的更是少之又少,比较好用的目前有两个
本系列分几部分,先对dig进行分析,第一篇介绍dig
的使用,第二篇再从源码来剖析他是如何通过返射实现的的依赖注入的,后续会介绍fx 的使用和实现原理。
dig主要的思路是能过Provider
将不同的函数注册到容器内,一个函数可以通过参数来声明对其他函数返回值的依赖。在Invoke
的时候才会真正的去调用容器内相应的Provider
方法。
dig
还提供了可视化的方法Visualize
用于生成dot
有向图代码,更直观的观察依赖关系,关于dot
的基本语法,可以查看帖子dot 语法总结
使用的dig
版本为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
里提供的函数,有多个函数返回的数据类型是一样的怎么处理?比如,我们的数据库有主从两个连接库,怎么进行区分?
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
里,和命名方式一样,有两种使用方式
第一种在调用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
来跑一下,他不会调用具体的函数,而是直接返回函数的返回参数的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) } }
运行代码不会有任何输出。