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


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

    • 谷歌出的wire,这个是用抽象语法树在编译时实现的。
    • uber出的dig,在运行时,用返射实现的,并基于dig库,写了一个依赖框架fx

    本系列分几部分,先对dig进行分析,第一篇介绍dig的使用,第二篇再从源码来剖析他是如何通过返射实现的的依赖注入的,后续会介绍fx 的使用和实现原理。
    dig主要的思路是能过Provider将不同的函数注册到容器内,一个函数可以通过参数来声明对其他函数返回值的依赖。在Invoke的时候才会真正的去调用容器内相应的Provider方法。
    dig还提供了可视化的方法Visualize用于生成dot有向图代码,更直观的观察依赖关系,关于dot的基本语法,可以查看帖子dot 语法总结
    帖子中的代码使用的digmaster分支,版本为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返回参数 Configdot 语法总结
    展示出来:

    命名参数--多个返回相同类型的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)
    	}
    }
    

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

    作者:李鹏
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    MonoRail学习-介绍篇(一)
    使用"_svn"替代".svn"的转换脚本
    Silverlight 4之ComboBox用法
    MD5 Algorithm
    子集算法的一个简单实现
    搜狗浏览器2.0正式版使用体验
    我做的抓屏软件
    WPF version of IPMessager
    Base64 Encode Decode Algorithm
    Origami Art
  • 原文地址:https://www.cnblogs.com/li-peng/p/14708132.html
Copyright © 2020-2023  润新知