Golang依賴注入框架wire全攻略

在前一陣介紹單元測試的系列文章中,曾經簡單介紹過wire依賴注入框架。但當時的wire還處於alpha階段,不過最近wire已經發布了首個beta版,API發生了一些變化,同時也承諾除非萬不得已,將不會破壞API的兼容性。在前文中,介紹了一些wire的基本概況,本篇就再也不重複,感興趣的小夥伴們能夠回看一下:搞定Go單元測試(四)—— 依賴注入框架(wire)。本篇將具體介紹wire的使用方法和一些最佳實踐。mysql

本篇中的代碼的完整示例能夠在這裏找到:wire-examplesgit

Installing

go get github.com/google/wire/cmd/wire
複製代碼

Quick Start

咱們先經過一個簡單的例子,讓小夥伴們對wire有一個直觀的認識。下面的例子展現了一個簡易wire依賴注入示例:github

$ ls
main.go  wire.go 
複製代碼

main.gosql

package main

import "fmt"

type Message struct {
	msg string
}
type Greeter struct {
	Message Message
}
type Event struct {
	Greeter Greeter
}
// NewMessage Message的構造函數
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter構造函數
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event構造函數
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
func (g Greeter) Greet() Message {
	return g.Message
}

// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}
/*
// 使用wire後
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}*/

複製代碼

wire.go數據庫

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import "github.com/google/wire"

// InitializeEvent 聲明injector的函數簽名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage)
	return Event{}  //返回值沒有實際意義,只需符合函數簽名便可
}
複製代碼

調用wire命令生成依賴文件:api

$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go  wire.go  wire_gen.go
複製代碼

wire_gen.go wire生成的文件bash

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent(msg string) Event {
	message := NewMessage(msg)
	greeter := NewGreeter(message)
	event := NewEvent(greeter)
	return event
}
複製代碼

使用前 V.S 使用後restful

...
/*
// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}*/

// 使用wire後
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}
...
複製代碼

使用wire後,只需調一個初始化方法既可獲得Event了,對比使用前,不只減小了三行代碼,而且無需再關心依賴之間的初始化順序。框架

示例傳送門: quickstartide

Provider & Injector

providerinjectorwire的兩個核心概念。

provider: a function that can produce a value. These functions are ordinary Go code.
injector: a function that calls providers in dependency order. With Wire, you write the injector's signature, then Wire generates the function's body.
github.com/google/wire…

經過提供provider函數,讓wire知道如何產生這些依賴對象。wire根據咱們定義的injector函數簽名,生成完整的injector函數,injector函數是最終咱們須要的函數,它將按依賴順序調用provider

在quickstart的例子中,NewMessage,NewGreeter,NewEvent都是providerwire_gen.go中的InitializeEvent函數是injector,能夠看到injector經過按依賴順序調用provider來生成咱們須要的對象Event

上述示例在wire.go中定義了injector的函數簽名,注意要在文件第一行加上

// +build wireinject
...
複製代碼

用於告訴編譯器無需編譯該文件。在injector的簽名定義函數中,經過調用wire.Build方法,指定用於生成依賴的provider:

// InitializeEvent 聲明injector的函數簽名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 傳入provider函數
	return Event{}  //返回值沒有實際意義,只需符合函數簽名便可
}
複製代碼

該方法的返回值沒有實際意義,只須要符合函數簽名的要求便可。

高級特性

quickstart示例展現了wire的基礎功能,本節將介紹一些高級特性。

接口綁定

根據依賴倒置原則(Dependence Inversion Principle),對象應當依賴於接口,而不是直接依賴於具體實現。

抽象成接口依賴更有助於單元測試哦!
搞定Go單元測試(一)——基礎原理
搞定Go單元測試(二)—— mock框架(gomock)

在quickstart的例子中的依賴均是具體實現,如今咱們來看看在wire中如何處理接口依賴:

// UserService 
type UserService struct {
	userRepo UserRepository // <-- UserService依賴UserRepository接口
}

// UserRepository 存放User對象的數據倉庫接口,好比能夠是mysql,restful api ....
type UserRepository interface {
	// GetUserByID 根據ID獲取User, 若是找不到User返回對應錯誤信息
	GetUserByID(id int) (*User, error)
}
// NewUserService *UserService構造函數
func NewUserService(userRepo UserRepository) *UserService {
	return &UserService{
		userRepo:userRepo,
	}
}

// mockUserRepo 模擬一個UserRepository實現
type mockUserRepo struct {
	foo string
	bar int
}
// GetUserByID UserRepository接口實現
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
	return &User{}, nil
}
// NewMockUserRepo *mockUserRepo構造函數
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
	return &mockUserRepo{
		foo:foo,
		bar:bar,
	}
}
// MockUserRepoSet 將 *mockUserRepo與UserRepository綁定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
複製代碼

在這個例子中,UserService依賴UserRepository接口,其中mockUserRepoUserRepository的一個實現,因爲在Go的最佳實踐中,更推薦返回具體實現而不是接口。因此mockUserRepoprovider函數返回的是*mockUserRepo這一具體類型。wire沒法自動將具體實現與接口進行關聯,咱們須要顯示聲明它們之間的關聯關係。經過wire.NewSetwire.Bind*mockUserRepoUserRepository進行綁定:

// MockUserRepoSet 將 *mockUserRepo與UserRepository綁定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))
複製代碼

定義injector函數簽名:

...
func InitializeUserService(foo string, bar int) *UserService{
	wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
	return nil
}
...
複製代碼

示例傳送門: binding-interfaces

返回錯誤

在前面的例子中,咱們的provider函數均只有一個返回值,但在某些狀況下,provider函數可能會對入參作校驗,若是參數錯誤,則須要返回errorwire也考慮了這種狀況,provider函數能夠將返回值的第二個參數設置成error:

// Config 配置
type Config struct {
    // RemoteAddr 鏈接的遠程地址
	RemoteAddr string
	
}
// APIClient API客戶端
type APIClient struct {
	c Config
}
// NewAPIClient  APIClient構造函數,若是入參校驗失敗,返回錯誤緣由
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二個參數設置成error
	if c.RemoteAddr == "" {
		return nil, errors.New("沒有設置遠程地址")
	}
	return &APIClient{
		c:c,
	},nil
}
// Service
type Service struct {
	client *APIClient
}
// NewService Service構造函數
func NewService(client *APIClient) *Service{
	return &Service{
		client:client,
	}
}
複製代碼

相似的,injector函數定義的時候也須要將第二個返回值設置成error

...
func InitializeClient(config Config) (*Service, error) { // <-- 第二個參數設置成error
	wire.Build(NewService,NewAPIClient)
	return nil,nil
}
...
複製代碼

觀察一下wire生成的injector

func InitializeClient(config Config) (*Service, error) {
	apiClient, err := NewAPIClient(config)
	if err != nil { // <-- 在構造依賴的順序中若是發生錯誤,則會返回對應的"零值"和相應錯誤
		return nil, err
	}
	service := NewService(apiClient)
	return service, nil
}
複製代碼

在構造依賴的順序中若是發生錯誤,則會返回對應的"零值"和相應錯誤。

示例傳送門: return-error

Cleanup functions

provider生成的對象須要一些cleanup處理,好比關閉文件,關閉數據庫鏈接等操做時,依然能夠經過設置provider的返回值來達到這樣的效果:

// FileReader
type FileReader struct {
	f *os.File
}
// NewFileReader *FileReader 構造函數,第二個參數是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
	f, err := os.Open(filePath)
	if err != nil {
	    return nil,nil,err
	}
	fr := &FileReader{
	    f:f,
	}
	fn := func() {
	    log.Println("cleanup") 
	    fr.f.Close()
	}
	return fr,fn,nil
}
複製代碼

跟返回錯誤相似,將provider的第二個返回參數設置成func()用於返回cleanup function,上述例子中在第三個參數中返回了error,但這是可選的:

wire對provider的返回值個數和順序有所規定:

  1. 第一個參數是須要生成的依賴對象
  2. 若是返回2個返回值,第二個參數必須是func()或者error
  3. 若是返回3個返回值,第二個參數必須是func(),第三個參數則必須是error

示例傳送門: cleanup-functions

Provider set

當一些provider一般是一塊兒使用的時候,可使用provider set將它們組織起來,以quickstart示例爲模板稍做修改:

// NewMessage Message的構造函數
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter構造函數
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event構造函數
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
// EventSet Event一般是一塊兒使用的一個集合,使用wire.NewSet進行組合
var EventSet  = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--
複製代碼

上述例子中將Event和它的依賴經過wire.NewSet組合起來,做爲一個總體在injector函數簽名定義中使用:

func InitializeEvent(msg string) Event{
	//wire.Build(NewEvent, NewGreeter, NewMessage)
	wire.Build(EventSet) 
	return Event{}
}
複製代碼

這時只需將EventSet傳入wire.Build便可。

示例傳送門: provider-set

結構體provider

除了函數外,結構體也能夠充當provider的角色,相似於setter注入:

type Foo int
type Bar int

func ProvideFoo() Foo {
	return 1
}
func ProvideBar() Bar {
	return 2
}
type FooBar struct {
	MyFoo Foo
	MyBar Bar
}
var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "MyFoo", "MyBar"))
複製代碼

經過wire.Struct來指定那些字段要被注入到結構體中,若是是所有字段,也能夠簡寫成:

var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "*")) // * 表示注入所有字段
複製代碼

生成的injector函數:

func InitializeFooBar() FooBar {
	foo := ProvideFoo()
	bar := ProvideBar()
	fooBar := FooBar{
		MyFoo: foo,
		MyBar: bar,
	}
	return fooBar
}
複製代碼

示例傳送門: struct-provider

Best Practices

區分類型

因爲injector的函數中,不容許出現重複的參數類型,不然wire將沒法區分這些相同的參數類型,好比:

type FooBar struct {
	foo string
	bar string
}

func NewFooBar(foo string, bar string) FooBar {
	return FooBar{
	    foo: foo,  
	    bar: bar,
	}
}
複製代碼

injector函數簽名定義:

// wire沒法得知入參a,b跟FooBar.foo,FooBar.bar的對應關係
func InitializeFooBar(a string, b string) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

複製代碼

若是使用上面的provider來生成injector,wire會報以下錯誤:

provider has multiple parameters of type string
複製代碼

由於入參均是字符串類型,wire沒法得知入參a,b跟FooBar.foo,FooBar.bar的對應關係。 因此咱們使用不一樣的類型來避免衝突:

type Foo string
type Bar string
type FooBar struct {
	foo Foo
	bar Bar
}

func NewFooBar(foo Foo, bar Bar) FooBar {
	return FooBar{
	    foo: foo,
	    bar: bar,
	}
}
複製代碼

injector函數簽名定義:

func InitializeFooBar(a Foo, b Bar) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}
複製代碼

其中基礎類型和通用接口類型是最容易發生衝突的類型,若是它們在provider函數中出現,最好統一新建一個別名來代替它(儘管還未發生衝突),例如:

type MySQLConnectionString string
type FileReader io.Reader
複製代碼

示例傳送門 distinguishing-types

Options Structs

若是一個provider方法包含了許多依賴,能夠將這些依賴放在一個options結構體中,從而避免構造函數的參數太多:

type Message string

// Options
type Options struct {
	Messages []Message
	Writer   io.Writer
	Reader   io.Reader
}
type Greeter struct {
}

// NewGreeter Greeter的provider方法使用Options以免構造函數過長
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
	return nil, nil
}
// GreeterSet 使用wire.Struct設置Options爲provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)
複製代碼

injector函數簽名:

func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
	wire.Build(GreeterSet)
	return nil, nil
}
複製代碼

示例傳送門 options-structs

一些缺點和限制

額外的類型定義

因爲wire自身的限制,injector中的變量類型不能重複,須要定義許多額外的基礎類型別名。

mock支持暫時不夠友好

目前wire命令還不能識別_test.go結尾文件中的provider函數,這樣就意味着若是須要在測試中也使用wire來注入咱們的mock對象,咱們須要在常規代碼中嵌入mock對象的provider,這對常規代碼有侵入性,不過官方彷佛也已經注意到了這個問題,感興趣的小夥伴能夠關注一下這條issue:github.com/google/wire…

更多參考

官方README.md
官方guide.md
官方best-practices.md

相關文章
相關標籤/搜索