分解uber依賴注入庫dig-使用篇

peng發表於2021-05-06

golang的依賴注入庫非常的少,好用的更是少之又少,比較好用的目前有兩個

  • 谷歌出的wire,這個是用抽象語法樹在編譯時實現的。
  • uber出的dig,在執行時,用返射實現的,並基於dig庫,寫了一個依賴框架fx

本系列分幾部分,先對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返回引數 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)
	}
}

執行程式碼不會有任何輸出。

相關文章