专栏首页吴亲强的深夜食堂为什么把 dig 迁移到 wire
原创

为什么把 dig 迁移到 wire

开篇

digwire 都是 Go 依赖注入的工具,那么,本质上功能相似的框架,为什么要从 dig 切换成 wire

场景

我们从场景出发。

假设我们的项目分层是:router->controller->service->dao

大概就长这样:

现在我们需要对外暴露一个订单服务的接口。

首页看 main.go 文件。

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/wuqinqiang/digvswire/dig"
	"github.com/wuqinqiang/digvswire/router"
)

func main() {
	serverStart()
}

func serverStart() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("init app err:%v\n", err)
		}
	}()
	e := gin.Default()
	di := dig.ContainerByDig()
	err := router.RegisterRouter(e, di)
	if err != nil {
		fmt.Printf("register router err:%v", err)
	}
	_ = e.Run(":8090")
}

这里使用了 gin 启动项目。 然后我们查看 dig.ContainerByDig()

dig

package dig

import (
	"github.com/wuqinqiang/digvswire/controller"
	"github.com/wuqinqiang/digvswire/dao"
	"github.com/wuqinqiang/digvswire/server"
	"go.uber.org/dig"
)

func ContainerByDig() *dig.Container {
	d := dig.New()
	_ = d.Provide(dao.NewOrderDao)
	_ = d.Provide(server.NewOrderServer)
	_ = d.Provide(controller.NewOrderHandler)
	return d
}

首先通过 dig.New() 创建一个 di 容器。 Provide 函数用于添加服务提供者, Provide 函数第一个参数本质上是一个函数。一个告诉容器 "我能提供什么,为了提供它,我需要什么?" 的函数。

比如我们看第二个 server.NewOrderServer,

package server

import (
	"github.com/wuqinqiang/digvswire/dao"
)

var _ OrderServerInterface = &OrderServer{}

type OrderServerInterface interface {
	GetUserOrderList(userId string) ([]dao.Order, error)
}

type OrderServer struct {
	orderDao dao.OrderDao
}

func NewOrderServer(order dao.OrderDao) OrderServerInterface {
	return &OrderServer{orderDao: order}
}

func (o *OrderServer) GetUserOrderList(userId string) (orderList []dao.Order, err error) {
	return o.orderDao.GetOrderListById(userId)
}

这里的 NewOrderServer(xxx)Provide 中的语意就是 "我能提供一个 OrderServerInterface 服务,但是我需要依赖一个 dao.OrderDao"。

刚才的代码中,

_ = d.Provide(dao.NewOrderDao)
_ = d.Provide(server.NewOrderServer)
_ = d.Provide(controller.NewOrderHandler)

因为我们的调用链是 controller->server->dao,那么本质上他们的依赖是 controller<-server<-dao,只是依赖的不是具体的实现,而是抽象的接口。

所以你看到 Provide 是按照依赖关系顺序写的。

其实完全没有必要,因为这一步 dig 只会对这些函数进行分析,提取函数的形参以及返回值。然后根据返回的参数来组织容器结构。 并不会在这一步执行传入的函数,所以在 Provide 阶段前后顺序并不重要,只要确保不遗漏依赖项即可。

万事俱备,我们开始注册一个能获取订单的路由,

err := router.RegisterRouter(e, d)

// router.go
func RegisterRouter(e *gin.Engine, dig *dig.Container) error {
	return dig.Invoke(func(handle *controller.OrderHandler) {
		e.GET("/user/orders", handle.GetUserOrderList)
	})
}

此时,调用 invoke, 才是真正需要获取 *controller.OrderHandler 对象。

调用 invoke 方法,会对传入的参数做分析,参数中存在 handle *controller.OrderHandler, 就会去容器中寻找哪个 Provide 进来的函数返回类型是 handle *controller.OrderHandler,

就能对应找到,

_ = d.Provide(controller.NewOrderHandler)
// 对应
func NewOrderHandler(server server.OrderServerInterface) *OrderHandler {
	return &OrderHandler{
		server: server,
	}
}

发现这个函数有形参 server.OrderServerInterface,那就去找对应返回此类型的函数,

_ = d.Provide(server.NewOrderServer)
//对应
func NewOrderServer(order dao.OrderDao) OrderServerInterface {
	return &OrderServer{orderDao: order}
}

又发现形参 (order dao.OrderDao),

_ = d.Provide(dao.NewOrderDao)
//对应
func NewOrderDao() OrderDao {
	return new(OrderDaoImpl)
}

最后发现 NewOrderDao 没有依赖,不需要再查询依赖。开始执行函数的调用 NewOrderDao(),把返回的 OrderDao 传入到上层的 NewOrderServer(order dao.OrderDao) 进行函数调用, NewOrderServer(order dao.OrderDao) 返回的 OrderServerInterface 继续返回到上层 NewOrderHandler(server server.OrderServerInterface) 执行调用,最后再把函数调用返回的 *OrderHandler 传递给 dig.Invoke(func(handle *controller.OrderHandler) {},

整个链路就通了。用一个简陋的图来描述这个过程

dig 的整个流程采用的是反射机制,在运行时计算依赖关系,构造依赖对象。

这样会存在什么问题?

假设我现在注释掉 Provide 的一行代码,比如,

func ContainerByDig() *dig.Container {
	d := dig.New()
	//_ = d.Provide(dao.NewOrderDao)
	_ = d.Provide(server.NewOrderServer)
	_ = d.Provide(controller.NewOrderHandler)
	return d
}

我们在编译项目的时候并不会报任何错误,只会在运行时才发现缺少了依赖项。

wire

还是上面的代码,我们使用 wire 作为我们的 DI 容器。

wire 也有两个核心概念: ProviderInjector

其中 Provider 的概念和 dig 的概念是一样的:"我能提供什么?我需要什么依赖"。

比如下面 wire.go 中的代码,

//+build wireinject

package wire

import (
	"github.com/google/wire"
	"github.com/wuqinqiang/digvswire/controller"
	"github.com/wuqinqiang/digvswire/dao"
	"github.com/wuqinqiang/digvswire/server"
)

var orderSet = wire.NewSet(
	dao.NewOrderDao,
	server.NewOrderServer,
	controller.NewOrderHandler)

func ContainerByWire() *controller.OrderHandler {
	wire.Build(orderSet)
	return &controller.OrderHandler{}
}

其中,dao.NewOrderDaoserver.NewOrderServer 以及 controller.NewOrderHandler 就是 Provider

你会发现这里还调用 wire.NewSet 把他们整合在一起,赋值给了一个变量 orderSet

其实是用到 ProviderSet 的概念。原理就是把一组相关的 Provider 进行打包。

这样的好处是:

  • 结构依赖清晰,便于阅读。
  • 以组的形式,减少 injector 里的 Build

至于 injector,本质上就是按照依赖关系调用 Provider 的函数,然后最终生成我们想要的对象(服务)。

比如上面的 ContainerByWire() 就是一个 injector

那么 wire.go 文件整体的思路就是:定义好 injector,然后实现所需的 Provider

最后在当前 wire.go 文件夹下执行 wire 命令后,

此时如果你的依赖项存在问题,那么就会报错提示。比如我现在隐藏上面的 dao.NewOrderDao,那么会出现

如果依赖不存在问题,最终会生成一个 wire_gen.go 文件。

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package wire

import (
	"github.com/google/wire"
	"github.com/wuqinqiang/digvswire/controller"
	"github.com/wuqinqiang/digvswire/dao"
	"github.com/wuqinqiang/digvswire/server"
)

// Injectors from wire.go:

func ContainerByWire() *controller.OrderHandler {
	orderDao := dao.NewOrderDao()
	orderServerInterface := server.NewOrderServer(orderDao)
	orderHandler := controller.NewOrderHandler(orderServerInterface)
	return orderHandler
}

// wire.go:

var orderSet = wire.NewSet(dao.NewOrderDao, server.NewOrderServer, controller.NewOrderHandler)

需要注意上面两个文件。我们看到 wire.go 中第一行 //+build wireinject ,这个 build tag 确保在常规编译时忽略 wire.go 文件。 而与之相对的 wire_gen.go 中的 //+build !wireinject。 两个对立的 build tag 是为了确保在任意情况下,两个文件只有一个文件生效, 避免出现 "ContainerByWire() 方法被重新定义" 的编译错误。

现在我们可以真正使用 injector 了,我们在入口文件中替换成 dig

func serverStart() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("init app err:%v\n", err)
		}
	}()
	e := gin.Default()
	err := router.RegisterRouterByWire(e, wire.ContainerByWire())
	if err != nil {
		panic(err)
	}
	_ = e.Run(":8090")
}
func RegisterRouterByWire(e *gin.Engine, handler *controller.OrderHandler) error {
	e.GET("/v2/user/orders", handler.GetUserOrderList)
	return nil
}

一切正常。

当然 wire 有一个点需要注意,在 wire.go 文件中开头几行:

//+build wireinject

package wire

build tagpackage 他们之间是有空行的,如果没有空行,build tag 识别不了,那么编译的时候就会报重复声明的错误:

还有很多高级的操作可以自行了解。

总结

以上大体介绍了 go 中 digwire 两个 DI 工具。其中 dig 是通过运行时反射实现的依赖注入。 而 wire 是根据自定义的代码,通过命令,生成相应的依赖注入代码,在编译期就完成依赖注入,无需反射机制。 这样的好处是:

  • 方便排查,如果存在依赖错误,编译时就能发现。而 dig 只能在运行时才能发现依赖错误。
  • 避免依赖膨胀,wire 生成的代码只包含被依赖的,而 dig 可能会存在好多无用依赖。
  • 依赖关系静态存在源码,便于工具分析。

Reference

[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

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • CrowdStrike为什么从Scala迁移到Go

    自 2012 年开始,安全公司 CrowdStrike 就使用 Scala 开发他们的应用程序,Scala 成为其技术栈的重要组成部分。但随着工程师团队由早期的...

    李海彬
  • 为什么要从 FMDB 迁移到 WCDB?

    希望通过本文全面地介绍 WCDB 和 FMDB 在使用方式、性能等方面的差异,以及迁移中可能遇到的问题,帮助开发者决定是否进行迁移。

    微信终端开发团队
  • 用什么策略把你的 JavaScript 迁移到 TypeScript

    本文概述了将代码库从 JavaScript 迁移到 TypeScript 的策略。还提到了需要进一步阅读的材料。

    疯狂的技术宅
  • Go 每日一库之 wire

    之前的一篇文章Go 每日一库之 dig介绍了 uber 开源的依赖注入框架dig。读了这篇文章后,@overtalk推荐了 Google 开源的wire工具。所...

    用户7731323
  • 为什么建议你要迁移到MySQL 8.0?

    MySQL 8.0 brings a lot of new features. These features make MySQL database much ...

    程序IT圈
  • 为什么我们要从 MySQL 迁移到 TiDB?

    60 云平台对 360 集团各大业务线均有提供服务支持,涉及的数据库支持方案有:MySQL、Redis、MongoDB、ES、GP、PiKA。

    PingCAP
  • 为什么你现在必须迁移到Python 3?

    2020年初,对Python 2的支持将停止。如果你没迁移到 Python 3,将面临各种风险,比如安全漏洞。幸好,从 Python 2 迁移到 Python ...

    程序IT圈
  • Uber为什么放弃Postgres选择迁移到MySQL?

    Uber 的早期架构包含了一个用 Python 开发的单体后端应用程序,这个应用程序使用 Postgres 作为数据存储。从那个时候开始,Uber 的架构已经发...

    深度学习与Python
  • golang依赖注入——wire

    最近在做golang的框架,发现golang同样需要类似java中spring一样的ioc依赖注入框架。

    Java架构师必看
  • 分解uber依赖注入库dig-使用篇

    本系列分几部分,先对dig进行分析,第一篇介绍dig的使用,第二篇再从源码来剖析他是如何通过返射实现的的依赖注入的,后续会介绍fx 的使用和实现原理。 di...

    lpxxn
  • Kratos技术系列|从Kratos设计看Go微服务工程实践

    ? ? 导读 github.com/go-kratos/kratos(以下简称Kratos)是一套轻量级 Go 微服务框架,致力于提供完整的微服务研发体验,整...

    腾讯云中间件团队
  • 为什么将信息管理系统迁移到云平台和SaaS?

    如今,信息管理市场正在发生根本性的变化。而将企业内容管理(ECM)交易转到云计算的进程则要慢得多,例如客户关系管理(CRM)套件。事实上在几年前,行业分析师估计...

    静一
  • DNS域名服务器,我们使用免费WIFI真的安全吗?

    KS Knowledge Sharing 知识分享 现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享 DNS内...

    互扯程序
  • 为什么吴恩达认为未来属于迁移学习?

    AI研习社按:日前,知名 AI 博主、爱尔兰国立大学 NLP 博士生 Sebastian Ruder 以 “迁移学习:机器学习的下一个前线” 为题,对迁移学习的...

    AI研习社
  • 好雨云帮一周问答集锦(12.05 - 12.11)

    Rainbond开源
  • 什么?WireGuard 可以让躲在 NAT 后面的客户端之间直连了??

    WireGuard 是由 Jason A. Donenfeld 等人创建的下一代开源 VPN 协议,旨在解决许多困扰 IPSec/IKEv2、OpenVPN 或...

    米开朗基杨
  • 企业迁移到多云或混合云到底意味着什么

    随着越来越多的企业将业务迁移到云中,采用云计算技术通常可以分为两类:混合云和多云。

    CloudBest
  • Golang Annotation 系统 - Gengo 实战

    代码生成的技术在各种语言中都很常用,尤其是静态语言,利用代码生成的技术可以实现一些大幅提高生产效率的工具。

    王磊-AI基础
  • kubernetes中常用对象service的详细介绍

    对于kubernetes整个集群来说,Pod的地址也可变的,也就是说如果一个Pod因为某些原因退出了,而由于其设置了副本数replicas大于1,那么该Pod就...

    极客运维圈

扫码关注云+社区

领取腾讯云代金券