https://github.com/google/wire
google wire是golang的一个依赖注入解决的工具,这个工具能够自动生成类的依赖关系。
当我们写代码的时候,都希望,类都是一个个独立的结构,互不耦合,而类之间的关系,是通过外部传递不同的依赖组件,来组装出来,生成不同的实例。
Why wire?
除了wire,Go的依赖注入框架还有Uber的dig和Facebook的inject,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection),而wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection)。使用反射带来的性能损失倒是其次,更重要的是反射使得代码难以追踪和调试(反射会令Ctrl+左键失效…)。而wire生成的代码是符合程序员常规使用习惯的代码,十分容易理解和调试。
关于wire的优点,在官方博文上有更详细的的介绍:blog.golang.org/wire
一个耦合的例子:
type BOB struct {
db *sql.DB
}
func NewBOB() *BOB{
return &BOB{
db: new(MysqlDB),
}
}
在例子中,BOB类会来依赖db类,而db类的生成,是通过new(MysqlDB)生成的,这样就把BOB和db类耦合起来了,如果要改变其他的数据库类,则需要改动BOB的代码了,这样是不合理的,因为BOB依赖的是DB,而不是MysqlDB,最好的方式,MysqlDB类改动了,但BOB代码不用改。
下面改一下代码,一个组装依赖的例子:
type BOB struct {
db *sql.DB
}
func NewBOB(db *sql.DB) *BOB{
return &BOB{
db: db,
}
}
func main(){
db := new(MysqlDB)
NewBOB(db)
}
在上面的例子里,原来的new(MysqlDB),替换为db: db,通过参数的方式来初始化db了,而类的组装是在main函数里面实现,使用这样组装,当MysqlDB改为XXDB的时候,BOB的代码不用变动,代码不用变动,出问题的几率就会少,这样的组装方式,就叫依赖注入了。
一般情况下,组装类的依赖关系,都可以手写,但平时写项目的时候,类的数量,很容易会有几十个,这个时候,如果手写的话,会很啰嗦麻烦,而且要理顺好生成的顺序。
为了解决依赖组装的问题,golang有uber的dig和Facebook的inject,这两个都是通过运行时注入的,使用运行时注入,会有一些问题,比如不好调试,错误提示不及时等,而wire采用不同的方式来实现,通过生成依赖注入的代码来解决问题,这样就和手写是一样的,只是减轻手写的麻烦。
wire可以通过类组装,结构体组装,还可以参数和异常返回,看看例子:
type Foo struct {
}
func NewFoo() *Foo{
return &Foo{}
}
type Bar struct {
foo *Foo
}
func NewBar(foo *Foo) *Bar {
return &Bar{
foo:foo,
}
}
func (p *Bar) Test(){
fmt.Println(“hello”)
}
wire.go:
package wire
import (
wire “github.com/google/wire”
)
type Instance struct {
Foo *Foo
Bar *Bar
}
var SuperSet = wire.NewSet(NewFoo, NewBar)
func InitializeAllInstance() *Instance {
wire.Build(SuperSet, Instance{})
return &Instance{}
}
执行wire命令,会读取到wire.NewSet里面的ProviderSet,通过分析各个函数的参数和返回值,来自行解决依赖,可以生成wire_gen.go:
func InitializeAllInstance() *Instance {
foo := NewFoo()
bar := NewBar(foo)
instance := &Instance{
Foo: foo,
Bar: bar,
}
return instance } 总结 通过google wire能够让手写依赖关系变得简单,而且相当直观,值得推荐使用。 https://segmentfault.com/a/1190000020955439?utm_source=tag-newest <!-- more --> $ go get github.com/google/wire/cmd/wire go: downloading github.com/google/wire v0.4.0 $ ~/go/bin/wire wire: go [list -e -json -compiled=true -test=false -export=false -deps=true -find=false -tags=wireinject -- .]: exit status 1: go: cannot find main module, but found .git/config in /Users/xiazemin/go/src/github.com/wire
to create a module there, run:
go mod init
wire: generate failed
$ go mod init
go: creating new go.mod: module github.com/wire
$ ~/go/bin/wire
wire: github.com/wire: wrote /Users/xiazemin/go/src/github.com/wire/wire_gen.go
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.
通过提供provider函数,让wire知道如何产生这些依赖对象。wire根据我们定义的injector函数签名,生成完整的injector函数,injector函数是最终我们需要的函数,它将按依赖顺序调用provider。
在quickstart的例子中,NewMessage,NewGreeter,NewEvent都是provider,wire_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{} //返回值没有实际意义,只需符合函数签名即可
}
复制代码
该方法的返回值没有实际意义,只需要符合函数签名的要求即可。
https://studygolang.com/articles/22266?fr=sidebar
https://juejin.cn/post/6844903853536575501
https://blog.csdn.net/ni_hao_fan/article/details/106097535
https://www.bilibili.com/read/cv4933623/
https://www.jianshu.com/p/58b3d96e1a04
https://blog.drewolson.org/go-dependency-injection-with-wire
https://studygolang.com/articles/27163?fr=sidebar
https://github.com/uber-go/dig
http://www.360doc.com/content/20/0227/15/33093582_895215170.shtml
package main
import (
“fmt”
“github.com/jessevdk/go-flags”
“go.uber.org/dig”
“gopkg.in/ini.v1”
)
type Option struct {
ConfigFile string short:"c" long:"config" description:"Name of config file."
}
func InitOption() (*Option, error) {
var opt Option
_, err := flags.Parse(&opt)
return &opt, err
}
func InitConf(opt Option) (ini.File, error) {
cfg, err := ini.Load(opt.ConfigFile)
return cfg, err
}
func PrintInfo(cfg *ini.File) {
fmt.Println(“App Name:”, cfg.Section(“”).Key(“app_name”).String())
fmt.Println(“Log Level:”, cfg.Section(“”).Key(“log_level”).String())
}
func main() {
container := dig.New()
container.Provide(InitOption)
container.Provide(InitConf)
container.Invoke(PrintInfo)
}
https://zhuanlan.zhihu.com/p/108518676
https://segmentfault.com/a/1190000021813270
依赖注入是软件工程中经常使用到的一种技术,它提供了一种控制反转的机制,把控制权利交给了调用方。调用方来决定使用哪些参数,哪些对象来进行具体的业务逻辑。
它有几个好处:
1 它让调用方更灵活。
2 大量减少定义类型的代码量
3 增加代码的可用性,因为调用方只需要关注它需要的参数,不需要顾及它不需要的参数了。
什么是依赖注入
依赖注入使用最多的应该是java中的spring框架了。依赖注入在使用的时候希望调用函数的参数是不固定的。
function Action(a TypeA, b TypeB)
就是说,这个Action在实际调用的时候,可以任意加参数,每次加一个参数类型,都有一个容器可以给这个Action调用函数传递对应的参数对象提供使用。
inject
Golang中也有项目是使用依赖注入实现的,martini就是一个依靠依赖注入实现的web框架,它的作者开源的https://github.com/codegangsta/inject 项目也就很值得我们学习。
这个inject项目很小,实际代码就一个文件,很容易阅读。
// Injector代表依赖注入的容器需要实现的接口
type Injector interface {
Applicator // 这个接口用来灌入到一个结构体
Invoker // 这个接口用来实际调用的,所以可以实现非反射的实际调用
TypeMapper // 这个接口是真正的容器
// SetParent sets the parent of the injector. If the injector cannot find a
// dependency in its Type map it will check its parent before returning an
// error.
SetParent(Injector) // 表示这个结构是递归的
}
这个Injector使用三个接口进行组合,每个接口有各自不同的用处。
TypeMapper是依赖注入最核心的容器部分,注入类型和获取类型都是这个接口承载的。
Invoker和Applicator都是注入部分,Invoker将TypeMapper容器中的数据注入到调用函数中。而Applicator将容器中的数据注入到实体对象中。
最后我们还将Injector容器设计为有层级的,在我们获取容器数据的时候,会先从当前容器找,找不到再去父级别容器中找。
这几个接口中的TypeMapper又值得看一下:
// TypeMapper represents an interface for mapping interface{} values based on type.
// TypeMapper是用来作为依赖注入容器的,设置的三种方法都是链式的
type TypeMapper interface {
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
// 直接设置一个对象,TypeOf是key,value是这个对象
Map(interface{}) TypeMapper
// Maps the interface{} value based on the pointer of an Interface provided.
// This is really only useful for mapping a value as an interface, as interfaces
// cannot at this time be referenced directly without a pointer.
// 将一个对象注入到一个接口中,TypeOf是接口,value是对象
MapTo(interface{}, interface{}) TypeMapper
// Provides a possibility to directly insert a mapping based on type and value.
// This makes it possible to directly map type arguments not possible to instantiate
// with reflect like unidirectional channels.
// 直接手动设置key和value
Set(reflect.Type, reflect.Value) TypeMapper
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
// the Type has not been mapped.
// 从容器中获取某个类型的注入对象
Get(reflect.Type) reflect.Value
}
这里的Map是将数据注入,即将数据类型和数据值进行映射存储在容器中。MapTo是将数据接口和数据值进行映射存储在容器中。Set就是手动将数据类型活着数据接口和数据值存储在容器中。Get则和Set相反。
我们可以看下inject文件中实现了这个接口的对象:injector
// 实际的注入容器,它实现了Injector的所有接口
type injector struct {
// 这个就是容器最核心的map
values map[reflect.Type]reflect.Value
// 这里设置了一个parent,所以这个Inject是可以嵌套的
parent Injector
}
其中的这个map[reflect.Type]reflect.Value就是最核心的。那么这里就需要注意到了,这个inject实际上是一个基础的map,而不是线程安全的map。所以如果在并发场景下,不应该在并发请求中进行动态注入或者改变容器元素。否则很有可能出现各种线程安全问题。
我们可以看看Map,Set等函数做的事情就是设置这个Map
i.values[reflect.TypeOf(val)] = reflect.ValueOf(val) 下一个重要的函数就Invoke。
这个Invoke做的事情我们也能很容易想清,根据它本身里面的函数参数类型,一个个去容器中拿对应值。
// 真实的调用某个函数f,这里的f默认是function
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
val := inj.Get(argType)
if !val.IsValid() {
return nil, fmt.Errorf("Value not found for type %v", argType)
}
in[i] = val
}
return reflect.ValueOf(f).Call(in), nil } 注:inject相关的中文注释代码解读在项目:https://github.com/jianfengye/inside-go 中。
go-macaron/inject
无闻在matini基础上又封装了一层inject。它使用的方法是直接保留CopyRight的通知,将https://github.com/codegangsta/inject 这个类做了一些修改。
我看了下这些修改,主要是增加了一个FastInvoker
// FastInvoker represents an interface in order to avoid the calling function via reflection.
//
// example:
// type handlerFuncHandler func(http.ResponseWriter, http.Request) error
// func (f handlerFuncHandler)Invoke([]interface{}) ([]reflect.Value, error){
// ret := f(p[0].(http.ResponseWriter), p[1].(http.Request))
// return []reflect.Value{reflect.ValueOf(ret)}, nil
// }
//
// type funcHandler func(int, string)
// func (f funcHandler)Invoke([]interface{}) ([]reflect.Value, error){
// f(p[0].(int), p[1].(string))
// return nil, nil
// }
type FastInvoker interface {
// Invoke attempts to call the ordinary functions. If f is a function
// with the appropriate signature, f.Invoke([]interface{}) is a Call that calls f.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke([]interface{}) ([]reflect.Value, error)
}
并且在Invoke调用的地方增加了一个分支,如果这个调用函数是自带有Invoke方法的,那么就用一种不用反射的方式。
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
switch v := f.(type) {
case FastInvoker:
return inj.fastInvoke(v, t, t.NumIn())
default:
return inj.callInvoke(f, t, t.NumIn())
}
}
我觉得这个fastInvoke是神来之笔啊。我们使用Golang的inject最害怕的就是性能问题。这里的Invoke频繁使用了反射,所以会导致Invoke的性能不会很高。但是我们有了fastInvoke替换方案,当需要追求性能的时候,我们就可以使用fastInvoke的方法进行替换。
示例
所以我下面的这个示例是最好的理解inject的例子:
package main
import “gopkg.in/macaron.v1”
import “github.com/go-macaron/inject”
import “fmt”
import “reflect”
type A struct {
Name string
}
type B struct {
Name string
}
func (b *B) GetName() string {
return b.Name
}
type I interface {
GetName() string
}
type C struct {
AStruct A inject
BStruct B inject
}
type MyFastInvoker func(arg1 A, arg2 I, arg3 string)
func (invoker MyFastInvoker) Invoke(args []interface{}) ([]reflect.Value, error) {
if a, ok := args[0].(A); ok {
fmt.Println(a.Name)
}
if b, ok := args[1].(I); ok {
fmt.Println(b.GetName())
}
if c, ok := args[2].(string); ok {
fmt.Println(c)
}
return nil, nil }
type Invoker2 struct {
inject.Injector
}
func main() {
InjectDemo()
a := &A{Name: "inject name"}
m := macaron.Classic()
m.Map(a)
m.Get("/", func(a *A) string {
return "Hello world!" + a.Name
})
m.Run() }
func InjectDemo() {
a := A{Name: “a name”}
inject1 := inject.New()
inject1.Map(a)
inject1.MapTo(&B{Name: “b name”}, (*I)(nil))
inject1.Set(reflect.TypeOf(“string”), reflect.ValueOf(“c name”))
inject1.Invoke(func(arg1 A, arg2 I, arg3 string) {
fmt.Println(arg1.Name)
fmt.Println(arg2.GetName())
fmt.Println(arg3)
})
c := C{}
inject1.Apply(&c)
fmt.Println(c.AStruct.Name)
inject2 := inject.New()
inject2.Map(a)
inject2.MapTo(&B{Name: "b name"}, (*I)(nil))
inject2.Set(reflect.TypeOf("string"), reflect.ValueOf("c name"))
inject2.Invoke(MyFastInvoker(nil)) }
输出:
a name
b name
c name
a name
b name
c name
上面那个例子能看懂基本就掌握了inject的使用了。
https://www.cnblogs.com/yjf512/p/12121345.html
https://www.cnblogs.com/marshhu/p/12955754.html
过去几年我一直使用Java进行开发,立即被Go生态系统中依赖注入(DI)背后的动力所打动。我决定尝试使用Uber的dig(https://github.com/uber-go/dig)库来构建我的项目,给我留下了深刻的印象。
我发现依赖注入DI帮助解决了我在以前的Go应用程序中遇到的很多问题 - 过度使用init函数,滥用全局变量和复杂的应用程序设置。
在本文中,我将介绍DI,然后通过一个示例,来看看使用DI框架(通过Uber的dig库)前后的不同。
DI的简要概述
依赖注入是你的组件(比如go语言中的structs)在创建时应该接收它的依赖关系的思想。这与在初始化期间构建其自己的依赖关系的组件的相关反模式相反。我们来看一个例子。
假设你有一个Server结构体需要一个Config结构体来实现它的行为。实现的一种方法就是在初始化期间Server自行构建Config。
type Server struct {
config *Config
}
func New() *Server {
return &Server{
config: buildMyConfigSomehow(),
}
}
这似乎很方便。组件调用者不必知道我们Server,甚至不需要操作Config。这对我们的组件调用者用户来说都是隐藏的。
但是,这用做有一些缺点。首先,如果我们想改变我们Config建造的方式,我们将不得不改变所有调用这个组件的代码。例如,假设我们的buildMyConfigSomehow功能现在需要一个参数。每个调用者都需要访问该参数,并需要将其传递到构建函数中。
另外,mock我们的行为变得非常困难Config。我们必须以某种方式进入我们的New功能内部,以便创建Config。
下面看看使用依赖注入的方式来实现:
type Server struct {
config *Config
}
func New(config *Config) *Server {
return &Server{
config: config,
}
}
现在我们Server的创造与创造的创造脱离了关系Config。我们可以使用任何我们想要创建的逻辑Config,然后将结果数据传递给我们的New函数。
此外,如果Config是一个接口,这给我们一个容易的嘲弄路线。New只要它实现我们的界面,我们就可以传递我们想要的任何东西。这使得我们Server用Config简单的模拟实现进行测试。
主要的缺点是,Config在我们可以创建之前必须手动创建它是一种痛苦Server。我们在这里创建了一个依赖图 - 我们必须创建我们的Config第一个,因为Server它依赖于它。在实际应用中,这些依赖关系图可能会变得非常大,并且会导致构建应用程序需要完成其工作的所有组件的复杂逻辑。
这是DI框架可以提供帮助的地方。DI框架通常提供两种功能:
一种“提供”新组件的机制。简而言之,这将告诉DI框架您需要构建自己的其他组件(您的依赖关系)以及在拥有这些组件后如何构建自己。
一种“检索”构建组件的机制。
DI框架通常基于您所讲述的“提供者”构建一个图并确定如何构建您的对象。这在摘要中很难理解,所以让我们通过一个中等大小的示例。
示例应用程序
我们将要审查一个HTTP服务器的代码,当客户端发出GET请求时,它会提供JSON响应/people。我们将逐个检查代码。为了简单起见,它全都存在于相同的包(main)中。请不要在真正的Go应用程序中执行此操作。这个例子的完整代码可以在这里找到。
首先,让我们看看我们的Person结构。除了一些JSON标签外,它没有任何行为。
type Person struct {
Id int json:"id"
Name string json:"name"
Age int json:"age"
}
A Person有Id,Name和Age。而已。
接下来让我们看看我们的Config。类似于Person,它没有依赖关系。不像Person,我们会提供一个构造函数。
type Config struct {
Enabled bool
DatabasePath string
Port string
}
func NewConfig() *Config {
return &Config{
Enabled: true,
DatabasePath: “./example.db”,
Port: “8000”,
}
}
Enabled告诉我们我们的应用程序是否应该返回实际数 DatabasePath告诉我们数据库在哪里(我们正在使用sqlite)。Port告诉我们将运行我们的服务器的端口。
这里是我们用来打开数据库连接的函数。它依赖于我们Config并返回一个*sql.DB。
func ConnectDatabase(config Config) (sql.DB, error) {
return sql.Open(“sqlite3”, config.DatabasePath)
}
接下来我们会看看我们的PersonRepository。该结构将负责从我们的数据库中提取人员并将这些数据库结果反序列化为合适的Person结构。
type PersonRepository struct {
database *sql.DB
}
func (repository PersonRepository) FindAll() []Person {
rows, _ := repository.database.Query(
SELECT id, name, age FROM people;
)
defer rows.Close()
people := []*Person{}
for rows.Next() {
var (
id int
name string
age int
)
rows.Scan(&id, &name, &age)
people = append(people, &Person{
Id: id,
Name: name,
Age: age,
}) }
return people
}
func NewPersonRepository(database *sql.DB) *PersonRepository {
return &PersonRepository{database: database}
}
PersonRepository需要建立数据库连接。它公开了一个单独的函数FindAll,它使用我们的数据库连接返回一个Person表示数据库中数据的结构列表。
为了在我们的HTTP服务器和PersonRepository我们之间提供一个图层,我们将创建一个PersonService。
type PersonService struct {
config *Config
repository *PersonRepository
}
func (service PersonService) FindAll() []Person {
if service.config.Enabled {
return service.repository.FindAll()
}
return []*Person{}
}
func NewPersonService(config *Config, repository *PersonRepository) *PersonService {
return &PersonService{config: config, repository: repository}
}
我们PersonService依赖于Config和PersonRepository。它公开了一个被称为“ FindAll有条件地调用PersonRepository应用程序是否被启用” 的函数。
最后,我们有我们的Server。这是负责运行一个HTTP服务器并委托给我们的合适的请求PersonService。
type Server struct {
config *Config
personService *PersonService
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc(“/people”, s.people)
return mux
}
func (s *Server) Run() {
httpServer := &http.Server{
Addr: “:” + s.config.Port,
Handler: s.Handler(),
}
httpServer.ListenAndServe()
}
func (s *Server) people(w http.ResponseWriter, r *http.Request) {
people := s.personService.FindAll()
bytes, _ := json.Marshal(people)
w.Header().Set(“Content-Type”, “application/json”)
w.WriteHeader(http.StatusOK)
w.Write(bytes)
}
func NewServer(config *Config, service *PersonService) *Server {
return &Server{
config: config,
personService: service,
}
}
这Server是依赖于PersonService和Config。
好的,我们知道我们系统的所有组件。现在我们究竟如何初始化它们并启动我们的系统呢?
可怕的main()
首先,让我们main()以旧式的方式编写我们的功能。
func main() {
config := NewConfig()
db, err := ConnectDatabase(config)
if err != nil {
panic(err)
}
personRepository := NewPersonRepository(db)
personService := NewPersonService(config, personRepository)
server := NewServer(config, personService)
server.Run()
}
首先,我们创造我们的Config。然后,使用Config,我们创建我们的数据库连接。从那里我们可以创造我们的PersonRepository这使我们能够创造我们的PersonService。最后,我们可以用它来创建Server并运行它。
唷,那很复杂。更糟的是,随着我们的应用程序变得越来越复杂,我们的main意志会越来越复杂。每当我们为我们的任何组件添加一个新的依赖关系时,我们都必须通过main函数中的顺序和逻辑来反映该依赖关系,以构建该组件。
正如您可能已经猜到的那样,依赖注入框架可以帮助我们解决这个问题。让我们来看看如何。
建立一个容器
术语“容器”通常用于DI框架中,用于描述添加“提供程序”的内容,您可以从中获取完整构建对象。该dig库为我们Provide提供了添加提供程序的Invoke功能以及从容器中检索完全构建的对象的功能。
首先,我们建立一个新的容器。
container := dig.New()
现在我们可以添加新的提供者。为此,我们调用Provide容器上的函数。它只需要一个参数:一个函数。该函数可以有任意数量的参数(表示要创建的组件的依赖关系)以及一个或两个返回值(表示该函数提供的组件以及可选的错误)。
container.Provide(func() *Config {
return NewConfig()
})
上面的代码说:“我为Config容器提供了一个类型,为了构建它,我不需要其他东西。” 现在我们已经展示了容器如何构建一个Config类型,我们可以使用它来构建其他类型。
container.Provide(func(config Config) (sql.DB, error) {
return ConnectDatabase(config)
})
这段代码说:“我为*sql.DB容器提供了一个类型,为了构建它,我需要一个Config。我也可以选择返回一个错误。”
在这两种情况下,我们都比所需的更冗长。因为我们已经有了NewConfig和ConnectDatabase定义的功能,我们可以直接使用他们作为供应商的容器。
container.Provide(NewConfig)
container.Provide(ConnectDatabase)
现在,我们可以要求容器给我们提供我们提供的任何类型的完全构建的组件。我们这样做使用该Invoke功能。该Invoke函数接受一个参数 - 一个包含任意数量参数的函数。函数的参数是我们希望容器为我们构建的类型。
container.Invoke(func(database *sql.DB) {
// sql.DB is ready to use here
})
容器做了一些非常聪明的东西。以下是发生的情况:
容器认识到我们要求一个 sql.DB
它确定我们的功能ConnectDatabase提供了这种类型
接下来确定我们的ConnectDatabase函数具有依赖性Config
它找到了Config这个NewConfig函数的提供者
NewConfig 没有任何依赖关系,所以它被调用
结果NewConfig是一个Config传递给ConnectDatabase
结果ConnectionDatabase是sql.DB被传递回调用者Invoke
这是容器为我们做的很多工作。事实上,它做得更多。该容器足够聪明,可以构建每个类型的一个实例,并且只有一个实例。这意味着如果我们在多个地方使用它(如多个存储库),我们绝不会意外创建第二个数据库连接。
更好的main()
现在我们知道dig容器的工作原理了,让我们用它来构建一个更好的主体。
func BuildContainer() *dig.Container {
container := dig.New()
container.Provide(NewConfig)
container.Provide(ConnectDatabase)
container.Provide(NewPersonRepository)
container.Provide(NewPersonService)
container.Provide(NewServer)
return container
}
func main() {
container := BuildContainer()
err := container.Invoke(func(server *Server) {
server.Run()
})
if err != nil {
panic(err)
}
}
我们以前没有见过的唯一的东西就是error来自的返回值Invoke。如果任何使用的提供者Invoke返回错误,我们的调用Invoke将停止并返回错误。
尽管这个例子很小,但应该很容易看出这种方法在我们的“标准”主体上的一些好处。随着我们的应用程序越来越大,这些好处变得更加明显。
最重要的好处之一就是创建我们的组件与创建它们的依赖关系的解耦。比如说,我们PersonRepository现在需要访问Config。我们所要做的就是改变我们的NewPersonRepository构造函数以包含Config作为参数。我们的代码中没有其他的变化。
其他的巨大好处是缺乏全局状态,缺少调用init(需要时懒惰地创建依赖关系,只创建一次,无需易错的init设置),并且易于对各个组件进行测试。想象一下,在测试中创建容器并要求完全构建的对象进行测试。或者,使用所有依赖关系的模拟实现来创建一个对象。所有这些在DI方法中都更容易。
一个值得传播的理念
我相信依赖注入有助于构建更强大和可测试的应用程序。随着这些应用程序规模的扩大,情况尤其如此。Go非常适合构建大型应用程序,并拥有一个很棒的DI工具dig。我相信Go社区应该拥抱DI并将其用于更多的应用程序中。
https://www.jianshu.com/p/cb3682ad34a7
https://www.dazhuanlan.com/2019/10/01/5d927074b5dd9/