Google开源wire使用(依赖注入)

简介

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

依赖注入

很讨厌依赖注入这个词,new一个类并使用,硬生生给弄个新名词来衬托好高端

看看各大博客八股文怎么说明

依赖注入是一种设计模式,用于管理对象之间的依赖关系。依赖注入的核心思想是将对象的依赖关系从代码中分离出来,从而使代码更加灵活和可维护。在依赖注入中,对象不再负责创建它所依赖的对象,而是由外部容器来负责创建和管理对象之间的依赖关系。

简单来说,在A类的需要调用另外B、C、D类等,现在A类调用E类即可,E类中统一实例化了B、C、D类

常规类依赖方式

构造函数注入

构造函数注入是最常见的依赖注入方式。在构造函数中,将依赖对象作为参数传递给对象的构造函数

type UserService struct {
    userRepository *UserRepository
}
 
func NewUserService(userRepository *UserRepository) *UserService {
    return &UserService{userRepository: userRepository}
}

UserService 依赖于 UserRepository

在 NewUserService 函数中,将 UserRepository 作为参数传递给 UserService 的构造函数,并将其保存在 UserService 结构体中。

这样,在创建 UserService 对象时,需要先创建 UserRepository 对象,并将其传递给 NewUserService 函数

另一种方式,属性注入

属性注入是将依赖对象作为对象属性进行注入的方式

type UserService struct {
    userRepository *UserRepository
}
 
func (u *UserService) SetUserRepository(userRepository *UserRepository) {
    u.userRepository = userRepository
}

UserService 依赖于 UserRepository。通过 SetUserRepository 方法将 UserRepository 注入到 UserService 中

再另一种方式,方法注入

方法注入是将依赖对象作为方法参数进行注入的方式

type UserService struct {}
 
func (u *UserService) SaveUser(user *User, userRepository *UserRepository) error {
    return userRepository.SaveUser(user)
}

UserService 依赖于 UserRepository。通过 SaveUser 方法将 UserRepository 注入到 UserService 中

全局变量

如果你想更简单些,且依赖的资源是线程安全的,也可直接定义个全局变量来存储类,这样整个项目直接调用该变量即可 更加的简单。特别适用于一些非脚手架框架,定制化比较高的轻量级框架 如Gin等

手动依赖注入的一些好处包括:

  1. 可以更轻松地追源码,因为依赖项显式传递给函数或构造函数,例如go-zero框架的svc
  2. 管理依赖项更简单
  3. 测试依赖于手动依赖项注入的代码可能更容易,尤其是在依赖项彼此松散耦合的情况下。

第三方依赖注入库使用,以 wire 为例

手动管理依赖其实挺好的,对一些有代码自动化洁癖、类多到爆炸的场景,可能有些不适用,Google的Wire,Facebook 的 Inject 和 Uber 的 Dig 都是些 star 数比较高的库

kratos中的wire应用

自我描述:wire是一个golang写的库,通过执行wire命令生成xx_gen.go文件帮你调用类的依赖

安装wire 一条命令就可以了

go get github.com/google/wire/cmd/wire
wire中有几个概念和常用方法
  • provider
    各目录下包中类注册

  • injector
    wire_gen.go文件中的注视,//+build wireinject注释确保了这个文件在我们正常编译的时候不会被引用,而 wire . 生成的文件 wire_gen.go 会包含 //+build !wireinject 注释,正常编译的时候,不指定 tag 的情况下会引用这个文件

  • wire.Build()
    函数用于声明依赖注入过程中需要使用的提供者

  • wire.NewSet()
    用于创建一个新的提供者集合

我们通过新增一个单独模块api接口来演示依赖注入

kratos框架目录结构,几个比较重要的目录

  1. api 定义接口服务的地方
  2. cmd 项目的main方法入口文件,一切的源头
  3. configs 静态配置文件
  4. internal 项目的核心文件,相当于大多数时间编码的地方
    4.1 biz 逻辑层,logic层
    4.2 conf 配置文件读取
    4.3 server http和grpc实例的创建和配置
    4.4 service api目录中定义的接口服务handle层
第一步:api目录中定义一个新服务

api/workbench/v1/workbench.proto

rpc ProjectCommonList(ListPortfolioRequest) returns (ListPortfolioReply){
    option (google.api.http) = {
        post: "/workbench/v1/commonproject/list"
        body: "*"
    };
}
第二步:生成对应的pb类

命令行执行

make api 
第三步:编写对应的接口handle方法

internal/service/workbench.go
CommercialService是新增的的logic

type WorkbenchService struct {
    v1.UnimplementedWorkbenchServer
    log                  *log.Helper
    userService          *biz.UserService
    fileService          *biz.FileService
    projectService       *biz.ProjectService
    commonProjectService *biz.CommonProjectService
    reviewService        *biz.ReviewService
    teamService          *biz.TeamService
    spaceService         *biz.SpaceService
    portfolioService     *biz.PortfolioService
    permService          *biz.PermService
    commercialService    *biz.CommercialService
}

func NewWorkbenchService(logger log.Logger, userService *biz.UserService, fileService *biz.FileService, projectService *biz.ProjectService,
    review *biz.ReviewService, team *biz.TeamService, space *biz.SpaceService, portfolio *biz.PortfolioService,
    perm *biz.PermService, commercial *biz.CommercialService, commomnproject *biz.CommonProjectService) *WorkbenchService {
    return &WorkbenchService{
        log:                  log.NewHelper(logger),
        userService:          userService,
        fileService:          fileService,
        projectService:       projectService,
        reviewService:        review,
        teamService:          team,
        spaceService:         space,
        portfolioService:     portfolio,
        permService:          perm,
        commercialService:    commercial,
        commonProjectService: commomnproject,
    }
}

func (w *WorkbenchService) ProjectCommonList(ctx context.Context, request *v1.ListPortfolioRequest) (*v1.ListPortfolioReply, error) {
    ret := &v1.ListPortfolioReply{
        Action:    request.Action,
        Model:     request.Model,
        RequestId: request.RequestId,
        UserIds:   make([]uint32, 0),
    }
    data, userIds, err := w.commonProjectService.ProjectCommonList(ctx, request)
    if err != nil {
        if se := new(kraErrors.Error); kraErrors.As(err, &se) {
            ret.Code = se.Code
            ret.Message = se.Message
            return ret, nil
        }
        w.log.Errorf("SetDefaultTab biz failed,err=%v", err)
        return nil, err
    }
    ret.Data = data
    ret.UserIds = userIds
    return ret, nil
}
第四步:编写对应的接口logic方法

biz文件夹中,新建commonproject.go文件
internal/biz/commonproject.go

package biz

import (
    "context"
    v1 "workbench/api/workbench/v1"

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

// CommonProjectService 项目相关接口
type CommonProjectService struct {
    log *log.Helper
}

func NewCommonProjectService(logger log.Logger) *CommonProjectService {
    return &CommonProjectService{
        log: log.NewHelper(logger),
    }
}

func (s *CommonProjectService) ProjectCommonList(ctx context.Context, request *v1.ListPortfolioRequest) (*v1.ListPortfolioReply_Data, []uint32, error) {
    return nil, nil, nil
}
第五步:将刚才新增的CommonProjectService.go类,注册到wire体系中

krato框架的每个目录下,都有个与文件夹同名的xxx.go文件,里面用来做wire的注册功能
internal/biz/biz.go

package biz

import (
    "workbench/internal/biz/conf"

    "github.com/google/wire"
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewFileService, NewProjectService, NewReviewService, NewUserService, NewTeamService, NewSpaceService, NewPermService, NewPortfolioService, NewCommercialService, conf.NewTabService, NewBroadcastScopeService, NewCommonProjectService)
第六步:生成wire依赖注入

cmd/workbench/ 入口目录下执行命令wire

wire
第七步:运行项目
make build 
./bin/workbench

curl 0.0.0.0:8001/workbench/v1/commonproject/list

步骤回顾

kratos框架中,习惯性的将每个目录下新建一个与文件夹同名的xx.go文件,用来注册该目录下的类,例如biz目录中的biz.go中通过wire.NewSet()注册该目录下的所有类

main.go为主注册入口,该文件夹中有个wire.go文件,通过wire.Build方法加载了各个目录下的所有依赖注册文件,通过wire命令生成了一个wire_gen.go文件[此文件不可编辑,由wire自身维护]

wire.go

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

main.go
在主文件中,通过调用wire_gen.go中的wireApp方法,即可实现所有的类的加载使用

app, cleanup, err := wireApp(bc.Server, bc.Data, bc.App, bc.Registry, bc.Auth, &plan, logger)

整体下来的注册流程为

  1. 每个目录下的同名文件目录名.go中通过wire.NewSet方法注册该目录下所有类,赋值给该目录下的ProviderSet变量
  2. 主文件目录下的wire.go文件,通过wire.Build方法加载各目录下注册的ProviderSet
  3. 通过wire命令生成wire_gen.go文件,主函数调用wire_gen.go文件中的wireApp即可实现类的全部依赖注入

整体来说,只用到了两个方法 wire.Build wire.NewSet

一个最简单的示例

操作步骤

  1. 新建wire.go文件,定义需要生成的代码模板,模板规则很简单,包括go代码、go generate、+build注释等
  2. 命令行运行wire,生成wire.go定义的业务wire_gen.go

目录结构

  • go.mod
  • go.sum
  • main.go
  • wire.go
  • wire_gen.go

代码如下

main.go

package main

import "fmt"

type Class_1 struct {
    msg string
}

type Class_2 struct {
    Class_1 Class_1
}

type Class_3 struct {
    Class_2 Class_2
}

// -----------Class_1 Class_2 Class_3的构造函数-----------
func NewClass_1(msg string) Class_1 {
    return Class_1{msg: msg}
}
func NewClass_2(m Class_1) Class_2 {
    return Class_2{Class_1: m}
}
func NewClass_3(g Class_2) Class_3 {
    return Class_3{Class_2: g}
}

// -----------Class_1 Class_2 Class_3的构造函数-----------

// -----------Class_1 Class_2 Class_3 业务代码-----------
func (e Class_3) Coding() {
    msg := e.Class_2.Coding3()
    fmt.Println(msg)
    e.Class_2.Coding2()
}
func (e Class_2) Coding2() {
    fmt.Println("Class_2 coding")
}
func (g Class_2) Coding3() Class_1 {
    return g.Class_1
}

// -----------Class_1 Class_2 Class_3 业务代码-----------

func main() {
    fmt.Println("=====不使用wire=====")
    class_1 := NewClass_1("hello world by source code")
    class_2 := NewClass_2(class_1)
    class_3 := NewClass_3(class_2)
    class_3.Coding()

    fmt.Println("=====使用wire=====")
    wire_class_3 := InitializeClass_3("hello_world by wire")
    wire_class_3.Coding()
}

wire.go

//go:build wireinject
// +build wireinject

// The build tag makes sure the stub is not built in the final build.

package main

import "github.com/google/wire"

// InitializeClass_3 声明injector的函数签名
func InitializeClass_3(msg string) Class_3 {
    wire.Build(NewClass_3, NewClass_2, NewClass_1)
    return Class_3{} //返回值没有实际意义,只需符合函数签名即可
}

生成的wire_gen.go

// Code generated by Wire. DO NOT EDIT.

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

package main

// Injectors from wire.go:

// InitializeClass_3 声明injector的函数签名
func InitializeClass_3(msg string) Class_3 {
    class_1 := NewClass_1(msg)
    class_2 := NewClass_2(class_1)
    class_3 := NewClass_3(class_2)
    return class_3
}

我们这里是单个文件依赖的演示,wire.Build的参数中直接写要注入的方法名就行

在工程项目例如 kratos 中,每个包下有个问价夹同名文件分别管理,通过wire.NewSet注册

再通过wire.go中的wire.Build构建代码,例如

data包 var ProviderSet = wire.NewSet(NewGreeterUsecase)

server包 var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer, NewWebsocketServer)

wire.go
wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp)

常见的错误

  1. 当通过wire.NewSet注入的依赖类,没有被调用时,wire命令生成的wire_gen.go代码是不会生成的

  2. 命令执行报错

wire: D:\site\my-kratos\cmd\my-kratos\wire.go:21:1: inject wireApp: no provider found for *my-kratos/internal/biz.UserService
        needed by *my-kratos/internal/biz.PermService in provider set "ProviderSet" (D:\site\my-kratos\internal\biz\biz.go:6:19)
        needed by *my-kratos/internal/service.WorkbenchService in provider set "ProviderSet" (D:\site\my-kratos\internal\service\service.go:6:19)
        needed by *github.com/go-kratos/kratos/v2/transport/grpc.Server in provider set "ProviderSet" (D:\site\my-kratos\internal\server\server.go:8:19)
        needed by *github.com/go-kratos/kratos/v2.App in provider "newApp" (D:\site\my-kratos\cmd\my-kratos\main.go:38:6)
wire: my-kratos/cmd/my-kratos: generate failed
wire: at least one generate failure

这种报错,NewUserService服务通过wire.NewSet注入时,需要biz.UserService依赖,而biz.UserService没有注册

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容