本文转载自微信公众号《吴沁强的深夜食堂》,作者吴沁库里。转载本文请联系吴勤强深夜食堂公众号。dig和wire都是Go的依赖注入工具。那么,为什么本质上功能相似的工具从dig切换到wire呢?场景让我们从场景开始。假设我们的项目层次是:router->controller->service->dao。该目录如下所示:现在我们需要向外界公开一个订单服务接口。查看主页上的main.go文件。这里使用gin来启动项目。dig然后我们看dig.ContainerByDig(),通过dig.New()创建一个di容器。Provide函数用于添加服务提供者,Provide函数的第一个参数本质上是一个函数。一个告诉容器“我能提供什么,我需要什么才能提供它?”的函数。我们看上面的server.NewOrderServer。这里Provide中NewOrderServer(xxx)的语义是“我可以提供一个OrderServerInterface服务,但是我需要依赖一个dao.OrderDao”。刚才的代码中,因为我们的调用链是controller->server->dao,那么本质上它们的依赖就是controller<-server<-dao,但是依赖的不是具体的实现,而是抽象接口。所以你看Provide是按照依赖顺序写的。其实完全没有必要,因为dig只会在这一步分析这些函数,提取出函数的形参和返回值。然后根据返回的参数组织容器结构。传入的函数不会在这一步执行,所以Provide阶段前后的顺序并不重要,只要确保没有遗漏任何依赖即可。一切准备就绪,我们开始注册一个可以拿到订单的路由。这个时候调用invoke才是真正需要获取*controller.OrderHandler对象。调用invoke方法会解析传入的参数。如果参数中有handle*controller.OrderHandler,就会去容器里找是哪个Provider进来的,函数的返回类型是handle*controller.OrderHandler,然后据此查找,发现是这个函数有形的参考server.OrderServerInterface,然后找到对应的返回这个类型的函数,找到形参(orderdao.OrderDao),最后发现NewOrderDao没有依赖关系,所以不需要查询依赖关系。开始执行函数调用NewOrderDao(),将返回的OrderDao传递给上层NewOrderServer(orderdao.OrderDao)进行函数调用,NewOrderServer(orderdao.OrderDao)返回的OrderServerInterface继续返回给上层NewOrderHandler(serverserver.OrderServerInterface)执行调用,最后将函数调用返回的*OrderHandler传递给dig.Invoke(func(handle*controller.OrderHandler){},整个链接就接通了。可能不太舒服看上面,用一张图来描述这个过程,dig整个过程采用了反射机制,在运行时计算依赖,构造依赖对象,会有什么问题?假设我现在注释掉一行Provide代码,对于举个例子,我们在编译项目的时候,不会报任何错误,缺少的依赖只会在运行时才找,wire还是上面的代码,我们使用wire作为我们的DI容器。wire也是有两个核心概念:Provider和Injector。其中,Provider的概念和dig的概念是一样的:“我能提供什么?我需要什么依赖”。比如下面wire.go中的代码,dao.NewOrderDaoserver.NewOrderServer和controller.NewOrderHandler就是Provider。你会发现这里也调用了wire.NewSet,将它们整合在一起,给一个变量orderSet赋值。其实就是利用了ProviderSet的概念。原理就是封装一组相关的Provider。这样做的好处是:结构依赖清晰易读。以组的形式,减少注入器中的Build。至于注入器,本质上就是根据依赖关系调用Provider的函数,最终生成我们想要的对象(服务)。例如,上面的ContainerByWire()是一个注入器。那么wire.go文件的整体思路就是:定义注入器,然后实现需要的Provider。最后,在当前wire.go文件夹下执行wire命令后,如果你的依赖有问题,会报错。比如我现在隐藏上面的dao.NewOrderDao,它就会出现。如果依赖没有问题,最终会生成一个wire_gen.go文件。需要注意上面两个文件。我们在wire.go中看到第一行//+buildwireinject,这个build标签确保在常规编译时忽略wire.go文件。相比之下,wire_gen.go中的//+build!wireinject。使用两个相反的构建标签来保证在任何情况下两个文件中只有一个生效,避免了“ContainerByWire()methodisredefined”的编译错误。现在我们可以实际使用注入器了,我们在入口文件中将其替换为dig。一切正常。当然,线材有一点要注意。在wire.go文件的前几行:buildtag和package之间有空行。如果没有空行,则无法识别build标签,编译时会报arepeatedstatement。误区:还有很多高级操作可以自己去学习。如果需要完整代码,请在下方留言。总结以上大致介绍了go中dig和wire这两个DI工具。其中dig是通过运行时反射的依赖注入。Wire则是基于自定义代码通过命令生成相应的依赖注入代码,在编译期完成依赖注入,无需反射机制。这样做的好处是:方便故障排除。如果有依赖错误,可以在编译时发现。而dig只能在运行时发现依赖错误。为了避免依赖扩展,wire生成的代码只包含依赖的东西,而dig可能有很多无用的依赖。依赖静态存在于源码中,方便工具分析。参考[1]https://github.com/google/wire[2]https://github.com/uber-go/dig[3]https://medium.com/@dche423/master-wire-cn-d57de86caa1b[4]https://www.cnblogs.com/li-peng/p/14708132.html
