简介
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等
手动依赖注入的一些好处包括:
- 可以更轻松地追源码,因为依赖项显式传递给函数或构造函数,例如go-zero框架的svc
- 管理依赖项更简单
- 测试依赖于手动依赖项注入的代码可能更容易,尤其是在依赖项彼此松散耦合的情况下。
第三方依赖注入库使用,以 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框架目录结构,几个比较重要的目录
- api 定义接口服务的地方
- cmd 项目的main方法入口文件,一切的源头
- configs 静态配置文件
- 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)
整体下来的注册流程为
- 每个目录下的同名文件
目录名.go中通过wire.NewSet方法注册该目录下所有类,赋值给该目录下的ProviderSet变量 - 主文件目录下的
wire.go文件,通过wire.Build方法加载各目录下注册的ProviderSet - 通过
wire命令生成wire_gen.go文件,主函数调用wire_gen.go文件中的wireApp即可实现类的全部依赖注入
整体来说,只用到了两个方法 wire.Build wire.NewSet
一个最简单的示例
操作步骤
- 新建
wire.go文件,定义需要生成的代码模板,模板规则很简单,包括go代码、go generate、+build注释等 - 命令行运行
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)
常见的错误
当通过
wire.NewSet注入的依赖类,没有被调用时,wire命令生成的wire_gen.go代码是不会生成的命令执行报错
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没有注册