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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 与其他依赖注入工具不同,比如 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
目录
相关文章
|
存储 数据可视化 搜索推荐
Kratos微服务轻松对接EFK日志系统
EFK 是一个完整的分布式日志收集系统,很好地解决了上述提到的日志收集难,检索和分析难的问题。EFK=Elasticsearch+Fluentd+Kibana
193 0
|
3月前
|
消息中间件 开发框架 Go
【揭秘】如何让Kratos微服务与NATS消息队列完美融合?看完这篇你就懂了!
【8月更文挑战第22天】Kratos是基于Go语言的微服务框架,提供全面工具助力开发者构建高性能应用。NATS作为轻量级消息队列服务,适用于分布式系统消息传递。本文详细介绍如何在Kratos项目中集成NATS,包括创建项目、安装NATS客户端、配置连接、初始化NATS、发送与接收消息等步骤,助您轻松实现高效微服务架构。
64 1
|
存储 缓存 API
Kratos微服务框架实现权鉴 - Zanzibar
用户的权限管理对每个项目来说都至关重要。不同的业务场景决定了不同的权限管理需求,不同的技术栈也有不同的解决方案。如果你面对一个非常复杂的业务,需要实现极为灵活的权限配置,并且同时对接多个服务怎么办呢?谷歌的一致性全球授权系统Zanzibar可以帮到你。
744 0
Kratos微服务框架实现权鉴 - Zanzibar
|
JSON 自然语言处理 Java
39-微服务技术栈(高级):分布式搜索引擎ElasticSearch(索引库、文档操作)
在前面读者朋友们可以了解到ES承载着和MySQL一样的“存储-查询”功能,那么就类似的会有建表语句、表结构、表数据,有了这些才可以存储-查询数据。而这些对应的在ES中是:Mapping映射(表结构-建表语句)、索引库(表本身)、文档(表数据)。本节笔者将带领大家完整上述概念的创建、使用。
189 0
|
前端开发 物联网 定位技术
Kratos微服务框架实现IoT功能:设备实时地图
IoT,也就是物联网,万物互联,在未来肯定是一个热点——实际上,现在物联网已经很热了。那好,既然这一块这么有前途。那我们就来学习怎么开发物联网系统吧。可是,作为一个小白,两眼一抹黑:我想学,可是我该如何开始?这玩意儿到底该咋整呢?在这个时候,我发现了B站开源的微服务框架[go-kratos](https://github.com/go-kratos/kratos)。那么,Kratos能否实现物联网的系统和功能呢?答案是:必须可以。
433 0
Kratos微服务框架实现IoT功能:设备实时地图
|
JSON Kubernetes Cloud Native
Kratos微服务框架实现权鉴 - OPA
现在,策略通常是它实际管理的软件服务的一个硬编码功能。Open Policy Agent让您可以将策略从软件服务中解耦出来,这样,负责策略的人员就可以从服务本身中分离出来,对策略进行读、写、分析、版本、发布以及一般的管理。OPA还为您提供了一个统一的工具集,使您可以将策略与任何您喜欢的软件服务解耦,并使用任何您喜欢的上下文来编写上下文感知策略。简而言之,OPA可以帮助您使用任何上下文从任何软件系统解耦任何策略。
647 0
|
前端开发 JavaScript 中间件
Kratos微服务框架实现权鉴 - Casbin
Casbin(<https://github.com/casbin/casbin>)是一套访问控制开源库,致力于帮助复杂系统解决权限管理的难题。同时也是一个国产开源项目。Casbin采用了元模型的设计思想,既支持ACL(访问控制列表),RBAC(基于角色访问控制),ABAC(基于属性访问控制)等经典的访问控制模型,也支持用户按照自身需求灵活定义权限。Casbin已经被Intel、IBM、腾讯云、VMware、RedHat、T-Mobile等公司开源使用,被Cisco、Verizon等公司闭源使用。具体详见Casbin主页(<https://casbin.org/>)。
538 0
|
Java Linux Go
Kratos微服务工程Bazel构建指南
Kratos是一个微服务框架,既然是微服务,那么一个工程下肯定会存在不少的服务,一个服务就是一个二进制可执行程序,那么我们将会面对一个问题:如何去构建(Build)这些服务程序。这件事情,通常都交由构建系统去做。我们能够选择的构建系统有很多:Make、CMake、Bazel……那么,我们又该如何选择一个构建系统呢?
1020 0
|
8天前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
44 6
|
8天前
|
设计模式 Java API
微服务架构演变与架构设计深度解析
【11月更文挑战第14天】在当今的IT行业中,微服务架构已经成为构建大型、复杂系统的重要范式。本文将从微服务架构的背景、业务场景、功能点、底层原理、实战、设计模式等多个方面进行深度解析,并结合京东电商的案例,探讨微服务架构在实际应用中的实施与效果。
25 1