Kratos微服务与它的小伙伴系列 - 依赖注入库 - Wire

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 与其他依赖注入工具不同,比如 Uber 的 Dig 和 Facebook 的 Inject,这 2 个工具都是使用反射实现的依赖注入,而且是运行时注入(runtime dependency injection)。

什么是依赖注入?

依赖注入 (Dependency Injection,缩写为 DI),是一种软件设计模式,也是实现控制反转(Inversion of Control)的其中一种技术。这种模式能让一个物件接收它所依赖的其他物件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”。此模式确保了任何想要使用给定服务的物件不需要知道如何建立这些服务。取而代之的是,连接收方物件(像是 client)也不知道它存在的外部代码(注入器)提供接收方所需的服务。

依赖注入涉及四个概念:

  1. 服务:任何类,提供了有用功能。
  2. 客户:使用服务的类。
  3. 接口:客户不应该知道服务实现的细节,只需要知道服务的名称和 API。
  4. 注入器:Injector,也称 assembler、container、provider 或 factory。负责把服务引入给客户。

依赖注入把对象构建与对象注入分开。因此创建对象的 new 关键字也可消失了。

Golang 的依赖注入框架有两类:

  • 通过反射在运行时进行依赖注入,典型代表是 Uber 开源的 dig
  • 通过 generate 进行代码生成,典型代表是 Google 开源的 wire

使用 dig 功能会强大一些,但是缺点就是错误只能在运行时才能发现,这样如果不小心的话可能会导致一些隐藏的 bug 出现。使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,并且生成的代码其实和我们自己手写相关代码差不太多,更符合直觉,心智负担更小,所以更加推荐 wire。

什么是Wire?

wire 是由 google 开源的一个供 Go 语言使用的依赖注入代码生成工具。它能够根据你的代码,生成相应的依赖注入 go 代码。

与其他依赖注入工具不同,比如 Uber 的 Dig 和 Facebook 的 Inject,这 2 个工具都是使用反射实现的依赖注入,而且是运行时注入(runtime dependency injection)。

wire 是编译代码时生成代码的依赖注入,是编译期间注入依赖代码(compile-time dependency injection)。而且代码生成期间,如果依赖注入有问题,生成依赖代码时就会出错,就可以报出问题来,而不必等到代码运行时才暴露出问题。

Provider 和 Injector

首先,需要理解 wire 的 2 个核心概念:provider 和 injector。

从上面 Java 模拟依赖注入的例子中,可以简化出依赖注入的步骤:

  • 第一:需要 New 出一个类实例
  • 第二:把这个 New 出来的类实例通过构造函数或者其他方式“注入”到需要使用它的类中
  • 第三:在类中使用这个 New 出来的实例

从上面步骤来理解 wire 的 2 个核心概念 provider 和 injector。

  • provider 就相当于上面 New 出来的类实例。
  • injector 就相当于“注入”动作前,把所需依赖函数进行聚合,根据这个聚合的函数生成依赖关系。

provider:提供一个对象。
injector:负责根据对象依赖关系,生成新程序。

Provider

Provider 是一个普通的 Go 函数 ,可以理解为是一个对象的构造函数。为下面生成 Injector 函数提供”构件“。

下面的 NewUserStore() 函数可以看作是一个 provider。这个函数需要传入 *Config*mysql.DB 2 个参数。

// NewUserStore 是一个 provider for *UserStore,*UserStore 依赖 *Config,*mysql.DB
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {... ...}

// NewDefaultConfig 是一个 provider for *Config,没有任何依赖
func NewDefaultConfig() *Config {...}

// NewDB 是 *mysql.DB 的一个 provider ,依赖于数据库连接信息 *ConnectionInfo
func NewDB(info *ConnectionInfo) (*mysql.DB, error){...}

provider 可以组合成一组 provider set。对于经常在一起使用的 providers 来说,这个非常有用。使用 wire.NewSet 方法可以把他们组合在一起,

var SuperSet = wire.NewSet(NewUserStore, NewDefaultConfig)

你也可以把其他的 provider sets 加入一个 provider set,

import (
    “example.com/some/other/pkg”
)

// ... ...
var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)
wire.NewSet() 函数:
这个函数可以把相关的 provider 组合在一起然后使用。当然也可以单独使用,如 var Provider = wire.NewSet(NewDB)。
这个 NewSet 函数的返回值也可以作为其他 NewSet 函数的参数使用,比如上面的 SuperSet 作为参数使用。

Injector

我们编写程序把这些 providers 组合起来(比如下面例子 initUserStore() 函数),wire 里的 wire 命令会按照依赖顺序调用 providers 生成更加完整的函数,这个就是 injector。

首先,编写生成 injector 的签名函数,然后用 wire 命令生成相应的函数。

例子如下:

// +build wireinject

func initUserStore(info *ConnectionInfo) (*UserStore, error) {
    wire.Build(SuperSet, NewDB) // 声明获取 UserStore 需要调用哪些 provider 函数
    return nil, nil
}

然后用 wire 命令把上面的 initUserStore 函数生成 injector 函数,生成的函数对应文件名 wire_gen.go

wire 命令:

You can generate the injector by invoking Wire in the package directory。
直接在生成 injector 函数的包下,使用 wire 命令,就可以生成 injector 代码。

wire.Build() 函数:

它的参数可以是 wire.NewSet() 组织的一个或多个 provider,也可以直接使用 provider。

与Kratos携起手来

Wire命令行工具安装

使用以下命令将Wire的命令行工具安装在全局路径下,用于代码的生成。

go install github.com/google/wire/cmd/wire@latest

场景代码

在这里,我们做一个“用户服务”。

根据Kratos的官方推荐Layout,我们将服务分为以下几层:server、service、biz、data。

package server

func NewHTTPServer(c *conf.Server, ac *conf.Auth, logger log.Logger, userSvc *service.UserService) *http.Server {
    var opts = []http.ServerOption{}
    if c.Http.Network != "" {
        opts = append(opts, http.Network(c.Http.Network))
    }
    if c.Http.Addr != "" {
        opts = append(opts, http.Address(c.Http.Addr))
    }
    if c.Http.Timeout != nil {
        opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
    }
    srv := http.NewServer(opts...)

    v1.RegisterUserServiceHTTPServer(srv, userSvc)

    return srv
}
package service

type UserService struct {
    v1.UnimplementedUserServiceServer

    uc  *biz.UserUseCase
    log *log.Helper
}

func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService {
    l := log.NewHelper(log.With(logger, "module", "service/user"))
    return &UserService{
        log: l,
        uc:  uc,
    }
}
package biz

type UserRepo interface {
    Create(ctx context.Context, req *v1.RegisterRequest) (*v1.User, error)
    Update(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error)
    Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error)
}

type UserUseCase struct {
    repo UserRepo
    log  *log.Helper
}

func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase {
    l := log.NewHelper(log.With(logger, "module", "user/usecase"))
    return &UserUseCase{
        repo: repo,
        log:  l,
    }
}
package data

var _ biz.UserRepo = (*UserRepo)(nil)

type UserRepo struct {
    data *Data
    log  *log.Helper
}

func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
    l := log.NewHelper(log.With(logger, "module", "user/repo"))
    return &UserRepo{
        data: data,
        log:  l,
    }
}

没有Wire,我们该如何组装代码?

现在,我们需要把上面这几个包组合起来,常规都是这样写的:

package main

func main() {
    userRepo := data.NewUserRepo(dataData, logger)
    userUseCase := biz.NewUserUseCase(userRepo, logger)
    userService := service.NewUserService(logger, userUseCase)
    httpSrv := server.NewHTTPServer(confServer, auth, logger, userService)

    app := kratos.New(
        kratos.Name("http"),
        kratos.Server(
            httpSrv,
        ),
    )
    if err := app.Run(); err != nil {
        log.Error(err)
    }
}

唔,看起来好像也没有什么啊,我觉着这么写也没啥问题啊。

是的,如果项目的规模很小的时候,这样写也没啥毛病,而且看起来还挺清晰的。

那么,我的项目没有这么简单了,突然爆炸了:

client := data.NewEntClient(confData, logger)
redisClient := data.NewRedisClient(confData, logger)
dataData, cleanup, err := data.NewData(client, redisClient, logger)
if err != nil {
    return nil, nil, err
}
userRepo := data.NewUserRepo(dataData, logger)
userUseCase := biz.NewUserUseCase(userRepo, logger)
userTokenRepo := data.NewUserTokenRepo(dataData, auth, logger)
userTokenUseCase := biz.NewUserAuthUseCase(userTokenRepo)
userService := service.NewUserService(logger, userUseCase, userTokenUseCase)
postRepo := data.NewPostRepo(dataData, logger)
postUseCase := biz.NewPostUseCase(postRepo, logger)
postService := service.NewPostService(logger, postUseCase)
linkRepo := data.NewLinkRepo(dataData, logger)
linkUseCase := biz.NewLinkUseCase(linkRepo, logger)
linkService := service.NewLinkService(logger, linkUseCase)
categoryRepo := data.NewCategoryRepo(dataData, logger)
categoryUseCase := biz.NewCategoryUseCase(categoryRepo, logger)
categoryService := service.NewCategoryService(logger, categoryUseCase)
commentRepo := data.NewCommentRepo(dataData, logger)
commentUseCase := biz.NewCommentUseCase(commentRepo, logger)
commentService := service.NewCommentService(logger, commentUseCase)
tagRepo := data.NewTagRepo(dataData, logger)
tagUseCase := biz.NewTagUseCase(tagRepo, logger)
tagService := service.NewTagService(logger, tagUseCase)
attachmentRepo := data.NewAttachmentRepo(dataData, logger)
attachmentUseCase := biz.NewAttachmentUseCase(attachmentRepo, logger)
attachmentService := service.NewAttachmentService(logger, attachmentUseCase)
httpServer := server.NewHTTPServer(confServer, auth, logger, userService, postService, linkService, categoryService, commentService, tagService, attachmentService)
registrar := server.NewRegistrar(registry)

现在,你再来看。我就问你,头大不大?脑壳晕不晕?心情美不美丽?

这是一个圆环套圆环的游戏,你不仅需要手写这么多的代码,而且,还需要管理他们之间的依赖关系,要小心翼翼的别把传入参数搞错、创建的顺序别搞错。

这时候,我要:增加一个方法,减少一个方法;增加一个变量,减少一个变量。都是很奔溃的事情。哪怕你再小心翼翼,也保不齐自己不出错。

有了Wire,我们可以如何组装代码?

首先需要在上面4个包下面声明4个ProviderSet变量:

package server

import (
    "github.com/google/wire"
)

// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewHTTPServer)
package service

import (
    "github.com/google/wire"
)

// ProviderSet is service providers.
var ProviderSet = wire.NewSet(
    NewUserService,
)
package biz

import "github.com/google/wire"

// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(
    NewUserUseCase,
)
package data

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
    NewData,

    NewEntClient,
    NewRedisClient,

    NewUserRepo,
)

而现在,main包下面,我需要两个go文件:

  • main.go
package main

func newApp(logger log.Logger, hs *http.Server, rr registry.Registrar) *kratos.App {
    return kratos.New(
        kratos.ID(Service.GetInstanceId()),
        kratos.Name(Service.Name),
        kratos.Version(Service.Version),
        kratos.Metadata(Service.Metadata),
        kratos.Logger(logger),
        kratos.Server(
            hs,
        ),
        kratos.Registrar(rr),
    )
}

func main() {
    app, cleanup, err := initApp(bc.Server, rc, bc.Data, bc.Auth, logger)
    if err != nil {
        panic(err)
    }
    defer cleanup()

    // start and wait for stop signal
    if err := app.Run(); err != nil {
        fmt.Println(err)
        panic(err)
    }
}
  • wire.go
//go:build wireinject
// +build wireinject

package main

import (
    "/internal/biz"
    "/internal/conf"
    "/internal/data"
    "/internal/server"
    "/internal/service"

    "github.com/go-kratos/kratos/v2"
    "github.com/go-kratos/kratos/v2/log"

    "github.com/google/wire"
)

// initApp init kratos application.
func initApp(*conf.Server, *conf.Registry, *conf.Data, *conf.Auth, log.Logger) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

然后在main包路径下直接运行wire命令:

$ wire
wire: XXXX: wrote XXXX\wire_gen.go

该命令将会在main包路径下生成一个wire_gen.go文件:

// Code generated by Wire. DO NOT EDIT.

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

package main


// Injectors from wire.go:

// initApp init kratos application.
func initApp(confServer *conf.Server, registry *conf.Registry, confData *conf.Data, auth *conf.Auth, logger log.Logger) (*kratos.App, func(), error) {
    client := data.NewEntClient(confData, logger)
    redisClient := data.NewRedisClient(confData, logger)
    dataData, cleanup, err := data.NewData(client, redisClient, logger)
    if err != nil {
        return nil, nil, err
    }
    userRepo := data.NewUserRepo(dataData, logger)
    userUseCase := biz.NewUserUseCase(userRepo, logger)
    userService := service.NewUserService(logger, userUseCase)
    httpServer := server.NewHTTPServer(confServer, auth, logger, userService)
    registrar := server.NewRegistrar(registry)
    app := newApp(logger, httpServer, registrar)
    return app, func() {
        cleanup()
    }, nil
}

明眼人的你一看就明白了:那些初始化依赖的代码全部都在生成的代码当中了。

从此,圆环套圆环,你调用我我调用你,依赖管理的这些脏活累活,你再也不需要接触,再也不需要干了,全部都丢给了Wire。

从此往后,你需要做什么呢?

维护每一个依赖包下面的ProviderSet,然后运行wire命令。

比如,我现在需要增加一个GRPC服务器,只需要在ProviderSet里边添加NewGRPCServer方法:

var ProviderSet = wire.NewSet(NewHTTPServer, NewGRPCServer, NewRegistrar)

然后运行wire命令,这时候wire_gen.go文件里边就会增加NewGRPCServer方法的调用。

再比如,我现在需要在NewHTTPServer方法增加一个变量,ProviderSet此时倒是不需要动的。但是,必须要执行wire命令,重新生成代码。

注意事项

wire 不允许不同的注入对象拥有相同的类型。google 官方认为这种情况,是设计上的缺陷。这种情况下,可以通过类型别名来将对象的类型进行区分。

func NewRegistrar(conf *conf.Registry) registry.Registrar

var ProviderSet = wire.NewSet(NewRegistrar, NewRegistrar)

以上的代码是不合法的,会报错ProviderSet has multiple bindings for ***

我们可以用下面的方法规避,但是,不建议这么做:

type RegistrarB registry.Registrar

func NewRegistrarA(conf *conf.Registry) registry.Registrar
func NewRegistrarB(conf *conf.Registry) RegistrarB

var ProviderSet = wire.NewSet(NewRegistrarA, NewRegistrarB)

结语

Wire 是一个强大的依赖注入工具。项目工程化过程中,Wire 可以很好的帮助我们管理依赖关系,协助我们完成复杂对象的构建组装。与此同时,Wire与 Inject 、Dig 等不同的是,Wire只生成代码,而不是使用反射在运行时注入,因此不需要担心会有性能损耗。

参考资料

  1. Wire Github
  2. Dig Github
  3. Go工程化(三) 依赖注入框架 wire
  4. 理解一下依赖注入,以及如何用 wire
  5. 依赖注入 - 维基百科
  6. Dependency Injection Demystified
  7. Go Cloud Wire:编译时依赖注入详解
  8. golang常用库包:Go依赖注入(DI)工具-wire使用
  9. Golang依赖注入框架wire使用详解
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
5月前
|
存储 数据可视化 搜索推荐
Kratos微服务轻松对接EFK日志系统
EFK 是一个完整的分布式日志收集系统,很好地解决了上述提到的日志收集难,检索和分析难的问题。EFK=Elasticsearch+Fluentd+Kibana
89 0
|
SQL 关系型数据库 MySQL
Kratos微服务与它的小伙伴系列 - ORM框架 - Ent
ent 是Facebook开源的一个简单但是功能强大的ORM框架,它可以轻松构建和维护具有大型数据模型的应用程序。它基于代码生成,并且可以很容易地进行数据库查询以及图遍历。
1411 0
|
JSON 自然语言处理 Java
39-微服务技术栈(高级):分布式搜索引擎ElasticSearch(索引库、文档操作)
在前面读者朋友们可以了解到ES承载着和MySQL一样的“存储-查询”功能,那么就类似的会有建表语句、表结构、表数据,有了这些才可以存储-查询数据。而这些对应的在ES中是:Mapping映射(表结构-建表语句)、索引库(表本身)、文档(表数据)。本节笔者将带领大家完整上述概念的创建、使用。
119 0
|
Java Linux Go
Kratos微服务工程Bazel构建指南
Kratos是一个微服务框架,既然是微服务,那么一个工程下肯定会存在不少的服务,一个服务就是一个二进制可执行程序,那么我们将会面对一个问题:如何去构建(Build)这些服务程序。这件事情,通常都交由构建系统去做。我们能够选择的构建系统有很多:Make、CMake、Bazel……那么,我们又该如何选择一个构建系统呢?
797 0
|
前端开发 小程序 开发工具
Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(六):客户端基础库 TS 实战
Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(六):客户端基础库 TS 实战
153 0
|
缓存 运维 微服务
【微服务No.2】polly微服务故障处理库
熔断、降级: 熔断:熔断就是我们常说的“保险丝”,意为当服务出现某些状况时,切断服务,从而防止应用程序不断地常识执行可能会失败的操作造成系统的“雪崩”,或者大量的超时等待导致系统卡死等情况,很多地方也将其成为“过载保护”。
1240 0
|
监控 微服务
构建微服务时,我们用到的库
本文讲的是构建微服务时,我们用到的库【编者的话】构建微服务时,究竟该不该使用库,又有那些代码适合写作库?这里是作者的一些经验之谈。
978 0
|
11天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
18 0
|
25天前
|
负载均衡 测试技术 持续交付
高效后端开发实践:构建可扩展的微服务架构
在当今快速发展的互联网时代,后端开发扮演着至关重要的角色。本文将重点探讨如何构建可扩展的微服务架构,以及在后端开发中提高效率的一些实践方法。通过合理的架构设计和技术选型,我们可以更好地应对日益复杂的业务需求,实现高效可靠的后端系统。
|
9天前
|
Kubernetes 安全 Java
构建高效微服务架构:从理论到实践
【4月更文挑战第9天】 在当今快速迭代与竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性及容错性,成为众多企业转型的首选。本文将深入探讨如何从零开始构建一个高效的微服务系统,覆盖从概念理解、设计原则、技术选型到部署维护的各个阶段。通过实际案例分析与最佳实践分享,旨在为后端工程师提供一套全面的微服务构建指南,帮助读者在面对复杂系统设计时能够做出明智的决策,并提升系统的可靠性与维护效率。