利用 uber-go/dig 庫管理依賴

機智的小小帥發表於2021-09-21

利用 uber-go/dig 庫管理依賴

github 地址

官方文件

介紹

dig 庫是一個為 go 提供依賴注入 (dependency injection) 的工具包,基於 reflection 實現的。

在專案中會涉及到很多物件,它們之間的依賴關係可能是這樣的

graph BT; A-->B; A-->C; B-->D; C-->D;

物件 D 的建立依賴於物件 B 和 物件 C

物件 B 和物件 C 的建立依賴於物件 A

func NewD(b *B,c *C) *D{}
func NewB(a *A)*B{}
func NewC(a *A)*C{}
func NewA() *A{}

如果在很多地方都需要使用者 D 物件,有兩個方法

  1. 從別的地方傳一個 D 物件過來
  2. 利用 NewD 重新生成一個新的 D 物件

在 Package 之間進行傳遞物件,可能會造成 Package 間的耦合,因此一般情況下我們會採取第二種方法。

但現在問題來了,D 物件的生成依賴於 B 物件和 C 物件,要想得到 B 物件和 C 物件,就需要呼叫 NewB 和 NewC 去生成,而 NewB 和 NewC 需要以 A 為引數,這就需要呼叫 NewA 來生成 A。專案中不可避免地會出現大量的 NewXXx() 這種方法的呼叫。dig 庫由此而生,按照官方的說法,它就是用來管理這種 物件圖 的。

Resolving the object graph during process startup

基本的使用方法

使用方法總結起來就三步:

  1. 使用 dig.New() 建立 digObj
  2. 呼叫 digObj.Provide() 提供依賴
  3. 呼叫 digObj.Invoke() 生成目標
type A struct{}
type B struct{}
type C struct{}
type D struct{}

func NewD(b *B, c *C) *D {
	fmt.Println("NewD()")
	return new(D)
}
func NewB(a *A) *B {
	fmt.Println("NewB()")
	return new(B)
}
func NewC(a *A) *C {
	fmt.Println("NewC()")
	return new(C)
}
func NewA() *A {
	fmt.Println("NewA()")
	return new(A)
}

func main() {
	// 建立 dig 物件
	digObj := dig.New()
	// 利用 Provide 注入依賴
	digObj.Provide(NewA)
	digObj.Provide(NewC)
	digObj.Provide(NewB)
	digObj.Provide(NewD)
	var d *D
	assignD := func(argD *D) {
		fmt.Println("assignD()")
		d = argD
	}
	fmt.Println("before invoke")
	// 根據提前注入的依賴來生成物件
	if err := digObj.Invoke(assignD); err != nil {
		panic(err)
	}
}
output:
before invoke
NewA()
NewB()
NewC()
NewD()
assignD()
Privide
func (c *Container) Provide(constructor interface{}, opts ...ProvideOption) error

我們可以把物件的構造想象成一個流水線車間,只要提供了原材料,以及原材料每一步的生成步驟,就可以生成最終的產品。例如在上面的物件圖中,原材料就是 A,生成步驟是 NewB,NewC,NewD ,最終得到的產物是 D。

Provide 就是用來提供依賴的,我們利用 Privode 提供生成步驟,Privode 接收一個 constructor ,construct 必須是一個 function obj,不能是一個普通的 Obj,否則 Invoke 時將會失敗

func main() {
	// 建立 dig 物件
	digObj := dig.New()
  a := new(A) 	// 這裡
	// 利用 Provide 注入依賴
	digObj.Provide(a)
	digObj.Provide(NewC)
	digObj.Provide(NewB)
	digObj.Provide(NewD)
	var d *D
	assignD := func(argD *D) {
		fmt.Println("assignD()")
		d = argD
	}
	fmt.Println("before invoke")
	// 根據提前注入的依賴來生成物件
	if err := digObj.Invoke(assignD); err != nil {
    // panic: missing type: *main.A
		panic(err)
	}
}

所以你僅僅想通過一個普通的 obj,請用一個 function 把它給包起來,並將這個 function 作為 constructor 傳入 Privode() 中。

另一個視角:上面的物件圖也可以從另一個角度去理解,原材料是空氣,生成步驟是 NewA,NewB,NewC,NewD,其中 NewA 描述瞭如何利用空氣來生成一個 A 物件

**函數語言程式設計的視角:一切物件都是函式,就拿 var a int 中的變數 a 來說,從某種意義上,也可以將它視為一個不接受任何引數並返回 a 本身的一個函式 **

Invoke
func (c *Container) Invoke(function interface{}, opts ...InvokeOption) error

invoke 會利用之前注入的依賴完成對 function 這個引數的呼叫,引數 function 同樣必須是一個 function obj,如果你希望利用 Invoke 來生成一個物件,就像這個物件提前生成,並在 function 中進行賦值。

Invoke 會返回 error,這個 error 是必須要處理的。

在呼叫 Invoke 方法時,才會真正執行 Provide 中提供的 constructor,且每個 constuctor 只有被呼叫一次。

type A struct {
}

func NewA() *A {
	fmt.Println("NewA()")
	return new(A)
}

func main() {
	digObj := dig.New()
	digObj.Provide(NewA)
	fmt.Println("before invoke")
	if err := digObj.Invoke(func(*A) {
		fmt.Println("hello")
	}); err != nil {
		panic(err)
	}
	if err := digObj.Invoke(func(*A) {
		fmt.Println("world")
	}); err != nil {
		panic(err)
	}
}
output:
before invoke
NewA()
hello
world
dig.In

有時候某個 struct 可能有很多依賴,這個 struct 的 constructor 可能會過長

func ConstructorA(*B,*C,*C,*E,*F){...}

這時可以將 dig.In 嵌入到 struct 內部,例如

type D struct {
	dig.In
	*B
	*C
}

效果與

// 注意這裡返回的是 D 而非 *D
func NewD(b *B, c *C) D {
	fmt.Println("NewD()")
	return new(D)
}

相同

type A struct{}
type B struct{}
type C struct{}
type D struct {
	dig.In
	*B
	*C
}

// here!!!
// func NewD(b *B, c *C) *D {
// 	fmt.Println("NewD()")
// 	return new(D)
// }

func NewB(a *A) *B {
	fmt.Println("NewB()")
	return new(B)
}
func NewC(a *A) *C {
	fmt.Println("NewC()")
	return new(C)
}
func NewA() *A {
	fmt.Println("NewA()")
	return new(A)
}

func main() {
	// 建立 dig 物件
	digObj := dig.New()
	// 利用 Provide 注入依賴
	digObj.Provide(NewA)
	digObj.Provide(NewC)
	digObj.Provide(NewB)
	// digObj.Provide(NewD) here!!!
	var d D	// not a pointer here!!!
	assignD := func(argD D) { // not a pointer here!!!
		fmt.Println("assignD()")
		d = argD
	}
	fmt.Println("before invoke")
	// 根據提前注入的依賴來生成物件
	if err := digObj.Invoke(assignD); err != nil {
		// panic missing type: *main.A
		panic(err)
	}
}
dig.Out

dig.Out 和 dig.In 是對稱的

  • dig.In 表示這個 struct 需要哪些依賴
  • dig.Out 表示這個 struct 可以提供哪些依賴
type BC struct {
	dig.Out
	*B
	*C
}

效果和

// 注意 argument bc 不是指標
func f(bc BC) (*B, *C) {
	return bc.B, bc.C
}

相同

type A struct{}
type B struct{}
type C struct{}
type D struct {
	dig.In
	*B
	*C
}

type BC struct {
	dig.Out
	*B
	*C
}

func NewBC(a *A) BC {
	return BC{}
}

func NewA() *A {
	fmt.Println("NewA()")
	return new(A)
}

func main() {
	// 建立 dig 物件
	digObj := dig.New()
	// 利用 Provide 注入依賴
	digObj.Provide(NewA)
	digObj.Provide(NewBC) // here
	var d D               // not a pointer here!!!
	assignD := func(argD D) { // not a pointer here!!!
		fmt.Println("assignD()")
		d = argD
	}
	fmt.Println("before invoke")
	// 根據提前注入的依賴來生成物件
	if err := digObj.Invoke(assignD); err != nil {
		// panic missing type: *main.A
		panic(err)
	}
}

一些實驗

注意 Provide 中 constructor 和 Invoke 中 function,他兩的引數和返回值必須要注意

  1. pointer 和 non-pointer 是不能混用的
  2. interface 和 inplement 也不能混用
case1: Proivde pointer, Invoke non-pointer
func main() {
	digObj := dig.New()
	digObj.Provide(func() *A {
		return new(A)
	})
	err := digObj.Invoke(func(A) {
		fmt.Println("hello")
	})
	if err != nil {
    // panic: missing dependencies for function "main".main.func2 
		panic(err)
	}
}
case2: Provide non-pointer, Invoke pointer
func main() {
	digObj := dig.New()
	digObj.Provide(func() A {
		return A{}
	})
	err := digObj.Invoke(func(*A) {
		fmt.Println("hello")
	})
	if err != nil {
    // panic: missing dependencies for function "main".main.func2
		panic(err)
	}
}
case3: Provide Implement, Invoke interface
type InterfaceA interface {
	Hello()
}

type ImplementA struct {
}

func (i ImplementA) Hello() {
	fmt.Println("hello A")
}

func main() {
	digObj := dig.New()
	digObj.Provide(func() ImplementA {
		return ImplementA{}
	})
	err := digObj.Invoke(func(a InterfaceA) {
		a.Hello()
	})
	if err != nil {
		// panic: missing dependencies for function "main".main.func2 
		panic(err)
	}
}

相關文章