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
如果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)
}
}
執行程式碼不會有任何輸出。