首页 > 其他分享 >Wire 的使用

Wire 的使用

时间:2024-03-11 14:55:20浏览次数:23  
标签:Wire return service wire func 使用 go

控制反转和依赖注入

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。依赖注入是生成灵活和松散耦合代码的标准技术,通过明确地向组件提供它们所需要的所有依赖关系。在 Go 中通常采用将依赖项作为参数传递给构造函数的形式:

构造函数NewBookRepo在创建BookRepo时需要从外部将依赖项db作为参数传入,我们在NewBookRepo中无需关注db的创建逻辑,实现了代码解耦。

// NewBookRepo 创建BookRepo的构造函数
func NewBookRepo(db *gorm.DB) *BookRepo {
	return &BookRepo{db: db}
}

区别于控制反转,如果在NewBookRepo函数中自行创建相关依赖,这将导致代码高度耦合并且难以维护和调试。

// NewBookRepo 创建BookRepo的构造函数
func NewBookRepo() *BookRepo {
  db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
	return &BookRepo{db: db}
}

为什么需要依赖注入工具

现在我们已经知道了应该在开发中尽可能地使用控制反转和依赖注入将程序解耦开来,从而写出灵活和易测试的程序。

在小型应用程序中,我们可以自行创建依赖并手动注入。但是在一个大型应用程序中,手动去实现所有依赖的创建和注入就会比较繁琐。

例如,在一些常见的HTTP服务中,会根据业务需要划分出不同的代码层:

├── internal
│   ├── conf
│   │   └── conf.go
│   ├── data
│   │   └── data.go
│   ├── server
│   │   └── server.go
│   └── service
│       └── service.go
└── main.go

我们的服务需要有一个配置,指定工作模式、连接的数据库和监听端口等信息。

// conf/conf.go

// NewDefaultConfig 返回默认配置,不需要依赖
func NewDefaultConfig() *Config {...}

我们这里定义了一个默认配置,当然后续可以支持从配置文件或环境变量读取配置信息。

在程序的data层,需要定义一个连接数据库的函数,它依赖上面定义的Config并返回一个*gorm.DB(这里使用gorm连接数据库)。

// data/data.go

// NewDB 返回数据库连接对象
func NewDB(cfg *conf.Config) (*gorm.DB, error) {...}

同时定义一个BookRepo,它有一些数据操作相关的方法。它的构造函数NewBookRepo依赖*gorm.DB,并返回一个*BookRepo

// data/data.go

type BookRepo struct {
	db *gorm.DB
}

func NewBookRepo(db *gorm.DB) *BookRepo {...}

Service层位于data层和Server层的中间,它负责实现对外服务。其中构造函数 NewBookService 依赖ConfigBookRepo

// service/service.go

type BookService struct {
	config *conf.Config
	repo   *data.BookRepo
}

func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}

server层又有一个NewServer构造函数,它依赖外部传入ConfigBookService

// server/server.go

type Server struct {
	config  *conf.Config
	service *service.BookService
}

func NewServer(cfg *conf.Config, srv *service.BookService) *Server {...}

main.go文件中又依赖Server创建一个app

// main.go

type Server interface {
	Run()
}

type App struct {
	server Server
}

func newApp(server Server) *App {...}

由于在程序中定义了大量需要依赖注入的构造函数,程序的main函数中会出现以下情形。所有依赖的创建和顺序都需要手动维护。

// main.go

func main() {
	cfg := conf.NewDefaultConfig()
	db, _ := data.NewDB(cfg)
	repo := data.NewBookRepo(db)
	bookSrv := service.NewBookService(cfg, repo)
	server := server.NewServer(cfg, bookSrv)
	app := newApp(server)

	app.Run()
}

我们确实需要一个工具来解决这类问题。

Wire

Wire 是一个专为依赖注入(Dependency Injection)设计的代码生成工具,它可以自动生成用于初始化各种依赖关系的代码,从而帮助我们更轻松地管理和注入依赖关系。

Wire 安装

我们可以执行以下命令来安装 Wire 工具:

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

安装之前请确保已将 $GOPATH/bin 添加到环境变量 $PATH 里。

Wire 的基本使用

前置代码准备

虽然我们在前面已经通过 go install 命令安装了 Wire 命令行工具,但在具体项目中,我们仍然需要通过以下命令安装项目所需的 Wire 依赖,以便结合 Wire 工具生成代码:

$ go get github.com/google/wire@latest

接下来,让我们模拟一个简单的 web 博客项目,编写查询文章接口的相关代码,并使用 Wire 工具生成代码。

项目的目录结构如下:

.
├── ioc
│   └── article.go
├── main.go
├── service
│   └── article.go
├── web
│   └── article.go
└── wire.go

首先,我们先定义相关类型与方法,并提供对应的 初始化函数

  • 定义 PostHandler 结构体,创建注册路由的方法 RegisterRoutes 和查询文章路由处理的方法 GetPostById 以及初始化的函数 NewPostHandler,并且它依赖于 IPostService 接口:
type PostHandler struct {
	serv service.IPostService
}

func (h *PostHandler) RegisterRoutes(engine *gin.Engine) {
	engine.GET("/post/:id", h.GetPostById)
}

func (h *PostHandler) GetPostById(ctx *gin.Context) {
	content := h.serv.GetPostById(ctx, ctx.Param("id"))
	ctx.String(http.StatusOK, content)
}

func NewPostHandler(serv service.IPostService) *PostHandler {
	return &PostHandler{serv: serv}
}
  • 定义 IPostService 接口,并提供了一个具体实现 PostService,接着创建 GetPostById 方法,用于处理查询文章的逻辑,然后提供初始化函数 NewPostService,该函数返回 IPostService 接口类型:
type IPostService interface {
	GetPostById(ctx context.Context, id string) string
}

var _ IPostService = (*PostService)(nil)

type PostService struct {
}

func (s *PostService) GetPostById(ctx context.Context, id string) string {
	return "欢迎访问博客"
}

func NewPostService() IPostService {
	return &PostService{}
}
  • 定义一个初始化 gin.Engine 函数 NewGinEngineAndRegisterRoute,该函数依赖于 *handler.PostHandler 类型,函数内部调用相关 handler 结构体的方法创建路由:
func NewGinEngineAndRegisterRoute(postHandler *web.PostHandler) *gin.Engine {
    engine := gin.Default()
    postHandler.RegisterRoutes(engine)
    return engine
}

使用 Wire 工具生成代码

前置代码已经准备好了,接下来我们编写核心代码,以便 Wire 工具能生成相应的依赖注入代码。

  • 首先我们需要创建一个 wire 的配置文件,通常命名为 wire.go。在这个文件里,我们需要定义一个或者多个注入器函数(Injector 函数,接下来的内容会对其进行解释),以便指引 Wire 工具生成代码。
func InitializeApp() *gin.Engine {
    wire.Build(
        web.NewPostHandler,
        service.NewPostService,
        ioc.NewGinEngineAndRegisterRoute,
    )

    return &gin.Engine{}
}

在上述代码中,我们定义了一个用于初始化 gin.Engine 的注入器函数,在该函数内部,我们使用了 wire.Build 方法来声明依赖关系,其中包括 PostHandlerPostService 和 InitGinEngine 作为依赖的构造函数。

wire.Build 的作用是 连接或绑定我们之前定义的所有初始化函数。当我们运行 wire 工具来生成代码时,它就会根据这些依赖关系来自动创建和注入所需的实例。

注意:文件首行必须加上 //go:build wireinject 或 // +build wireinject(go 1.18 之前的版本使用) 注释,作用是只有在使用 wire 工具时才会编译这部分代码,其他情况下忽略。

  • 接下来在 wire.go 文件所处目录下执行 wire 命令,生成 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

import (
	"github.com/gin-gonic/gin"
	"golang-example/wire/blog/ioc"
	"golang-example/wire/blog/service"
	"golang-example/wire/blog/web"
)

// Injectors from wire.go:

func InitializeApp() *gin.Engine {
	iPostService := service.NewPostService()
	postHandler := web.NewPostHandler(iPostService)
	engine := ioc.NewGinEngineAndRegisterRoute(postHandler)
	return engine
}

生成的代码和我们手写区别不大,当我们的组件很多,依赖关系复杂的时候,我们才会感觉到 Wire 工具的好处。

Wire 的核心概念

Wire 有两个核心概念:提供者(providers)和注入器(injectors)。

Wire 提供者(providers)

提供者:一个可以产生值的函数,也就是有返回值的函数。例如入门代码里的 NewPostHandler 函数:

func NewPostHandler(serv service.IPostService) *PostHandler {  
	return &PostHandler{serv: serv}  
}

返回值不仅限于一个,如果有需要的话,可以额外添加一个 error 的返回值。

如果提供者过多的时候,我们还可以以分组的形式进行连接,例如将 post 相关的 handler 和 service 进行组合:

package web  
  
var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)

使用 wire.NewSet 函数将提供者进行分组,该函数返回一个 ProviderSet 结构体。不仅如此,wire.NewSet 还能对多个 ProviderSet 进行分组 wire.NewSet(PostSet, XxxSet) 。

对于之前的 InitializeApp 函数,我们可以这样升级:

func InitializeAppV2() *gin.Engine {  
	wire.Build(  
		web.PostSet,  
		ioc.NewGinEngineAndRegisterRoute,  
	)  
	return &gin.Engine{}  
}

然后通过 Wire 命令生成代码,和之前的结果一致。

Wire 注入器(injectors)

注入器(injectors)的作用是将所有的提供者(providers)连接起来,回顾一下我们之前的代码:

func InitializeApp() *gin.Engine {  
	wire.Build(  
		web.NewPostHandler,  
		service.NewPostService,  
		ioc.NewGinEngineAndRegisterRoute,  
	)  
	return &gin.Engine{}  
}

InitializeApp 函数就是一个注入器,函数内部通过 wire.Build 函数连接所有的提供者,然后返回 &gin.Engine{},该返回值实际上并没有使用到,只是为了满足编译器的要求,避免报错而已,真正的返回值来自 ioc.NewGinEngineAndRegisterRoute

Wire 高级应用

绑定接口

回顾我们之前编写的代码:

package web

···

func NewPostHandler(serv service.IPostService) *PostHandler {
    return &PostHandler{serv: serv}
}

···

pakacge service

···

func NewPostService() IPostService {
    return &PostService{}
}

···

NewPostHandler 函数依赖于 service.IPostService 接口,NewPostService 函数返回的是 IPostService 接口的值,这两个地方的类型匹配,因此 Wire 工具能够正确识别并生成代码。然而,这并不是推荐的最佳实践。因为在 Go 中的 最佳实践 是返回 具体的类型 的值,所以最好让 NewPostService 返回具体类型 PostService 的值:

func NewPostService() *PostService {  
	return &PostService{}  
}

但是这样,Wire 工具将认为 IPostService 接口类型与 PostService 类型不匹配,导致生成代码失败。因此我们需要修改注入器的代码:

func InitializeApp() *gin.Engine {  
	wire.Build(  
		web.NewPostHandler,  
		service.NewPostService,  
		ioc.NewGinEngineAndRegisterRoute,  
		wire.Bind(new(service.IPostService), new(*service.PostService)),  
	)  
	return &gin.Engine{}  
}

使用 wire.Bind 来建立接口类型和具体的实现类型之间的绑定关系,这样 Wire 工具就可以根据这个绑定关系进行类型匹配并生成代码。

wire.Bind 函数的第一个参数是指向所需接口类型值的指针,第二个实参是指向实现该接口的类型值的指针。

结构体提供者(Struct Providers)

Wire 库有一个函数是 wire.Struct,它能根据现有的类型进行构造结构体,我们来看看下面的例子:

package main

type Name string

func NewName() Name {
    return"Jack"
}

type PublicAccount string

func NewPublicAccount() PublicAccount {
    return"Hello World"
}

type User struct {
    MyName          Name
    MyPublicAccount PublicAccount
}

func InitializeUser() *User {
    wire.Build(
       NewName,
       NewPublicAccount,
       wire.Struct(new(User), "MyName", "MyPublicAccount"),
    )
    return &User{}
}

上述代码中,首先定义了自定义类型 Name 和 PublicAccount 以及结构体类型 User,并分别提供了 Name 和 PublicAccount 的初始化函数(providers)。然后定义一个注入器(injectorsInitializeUser,用于构造连接提供者并构造 *User 实例。

使用 wire.Struct 函数需要传递两个参数,第一个参数是结构体类型的指针值,另一个参数是一个可变参数,表示需要注入的结构体字段的名称集。

根据上述代码,使用 Wire 工具生成的代码如下所示:

func InitializeUser() *User {
    name := NewName()
    publicAccount := NewPublicAccount()
    user := &User{
       MyName:          name,
       MyPublicAccount: publicAccount,
    }
    return user
}

如果我们不想返回指针类型,只需要修改 InitializeUser 函数的返回值为非指针即可。

绑定值

有时候,我们可以在注入器中通过 值表达式 给一个类型进行赋值,而不是依赖提供者(providers)。

func InjectUser() User {  
	wire.Build(wire.Value(User{MyName: "Jack"}))  
	return User{}  
}

在上述代码中,使用 wire.Value 函数通过表达式直接指定 MyName 的值,生成的代码如下所示:

func InjectUser() User {
    user := _wireUserValue
    return user
}

var (
    _wireUserValue = User{MyName: "Jack"}
)

需要注意的是,值表达式将被复制到生成的代码文件中。

对于接口类型,可以使用 InterfaceValue

func InjectPostService() service.IPostService {
    wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))
    return nil
}

使用结构体字段作为提供者(providers)

有些时候,你可以使用结构体的某个字段作为提供者,从而生成一个类似 GetXXX 的函数。

func GetUserName() Name {
    wire.Build(
       NewUser,
       wire.FieldsOf(new(User), "MyName"),
    )
    return ""
}

你可以使用 wire.FieldsOf 函数添加任意字段,生成的代码如下所示:

func GetUserName() Name {
    user := NewUser()
    name := user.MyName
    return name
}

func NewUser() User {
    return User{MyName: Name("Jack"), MyPublicAccount: PublicAccount("HelloWorld")}
}

清理函数

如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。

并且 Wire 对 Provider 的返回值个数及顺序有以下限制:

  • 第一个返回值是需要生成的对象
  • 如果有 2 个返回值,第二个返回值必须是 func() 或 error
  • 如果有 3 个返回值,第二个返回值必须是 func(),而第三个返回值必须是
// db.go
func InitGormDB()(*gorm.DB, func(), error) {
    // 初始化db链接
    // ...
    cleanFunc := func(){
        db.Close()
    }

    return db, cleanFunc, nil
}

// wire.go
func BuildInjector() (*Injector, func(), error) {
   wire.Build(
      common.InitGormDB,
      // ...
      NewInjector
   )

   return new(Injector), nil, nil
}

// 生成的wire_gen.go
func BuildInjector() (*Injector, func(), error) {
   db, cleanup, err := common.InitGormDB()
   // ...
   return injector, func(){
       // 所有provider的清理函数都会在这里
       cleanup()
   }, nil
}

// main.go
injector, cleanFunc, err := app.BuildInjector()
defer cleanFunc()

备用注入器语法

如果你不喜欢将类似这种写法 → return &gin.Engine{} 放在你的注入器函数声明的末尾,你可以用 panic 来更简洁地写它:

func InitializeGin() *gin.Engine {  
	panic(wire.Build(/* ... */))  
}

总结

在本文中,我们详细探讨了 Go Wire 工具的基本用法和高级特性。它是一个专为依赖注入设计的代码生成工具,它不仅提供了基础的依赖解析和代码生成功能,还支持多种高级用法,如接口绑定和构造结构体。

依赖注入的设计模式应用非常广泛,Wire 工具让依赖注入在 Go 语言中变得更简单。

本文的所有代码在这里

标签:Wire,return,service,wire,func,使用,go
From: https://www.cnblogs.com/gui-lin/p/18066085

相关文章

  • Git使用
    1、设置git用户名和邮箱(1)用户名和邮箱地址是本地git客户端的一个变量,每次commit都会用用户名和邮箱纪录。github的contributions统计就是按邮箱来统计的。$gitconfig--globaluser.name"${你的用户名}"$gitconfig--globaluser.email"${你的邮箱}"(2)通过下述命......
  • 使用威联通做UPS server(NUT) 配置 linux和windows的 UPS client
     NetworkUPSTools(NUT)威联通设置1️⃣威联通使用usb线接入UPS2️⃣在设置中勾选“启用网络不间断服务器”,启用UPS服务器3️⃣把需要连入的设备IP加到列表里如下图所示配置linux以Ubuntu为例,其他使用NUT自行安装1️⃣安装NUTaptinstallnut#启动NUT生成默认......
  • 【触想智能】工业触摸显示器在户外使用需要注意哪些问题?
    工业显示器是智能制造领域应用比较广泛的电子产品,它广泛应用于工厂产线以及各种配套设备,在很大程度上提升了工厂的生产效率。工业显示器按触摸方式分,可以分为工业触摸显示器和非触摸工业显示器两种;按使用环境分,又可以分为室内工业显示器和户外工业显示器。由于室内和户......
  • uni-forms 和 uni-data-select 嵌套使用设定必填但初始化时不校验
    使用 uni-forms-item和 uni-data-select嵌套时,首先要注意的是:name=[]属性必须使用动态绑定,而且数组中的字符串必须和v-model的属性完全相同,如v-model="formatStationInfo.requiredSelect[index].curValue":name="['requiredSelect',index,'curValue']" :......
  • 如何使用screen解决ssh断连训练停止的问题
    screen是一个强大的终端多路复用器,允许你在一个SSH会话中运行多个终端任务,并且即使SSH连接中断,这些任务也能继续运行。这对长时间运行的任务或训练任务非常有用,如果在这样的任务中SSH链接断开,没有screen,你会丢失对任务的控制,而有了screen,你可以在链接恢复后接着控制之前的任务。......
  • 在Windows server 2012R2系统安装使用docker
    REF:https://blog.csdn.net/user_san/article/details/121037022需要进行配置,否则无法将端口映射出来,导致连接不上数据库。另外MYSQL8.0签权方式改变,无法通过navicat连接,需要修改ALTERUSER'root'@'%'IDENTIFIEDWITHmysql_native_passwordBY'123123';FLUSHPRIVILEGES......
  • 搭建交换机模拟环境及SSH连接,华为NSP软件入门使用教程
    如果你是通过搜索搜到了这篇文章,那么一定是工作或者学习中需要用交换机,但是又没物理机测试学习,所以需要搭建本地的虚拟环境学习。这篇文章是我进行交换机命令入门学习写的,笔者之前也是网上搜索,关于交换机的内容实在太少了。所以记录下来,给后来者少走弯路1.华为ENSP软件下载官......
  • 教您如何使用API接口获取虾皮商品详情
    要使用API接口获取虾皮(Shopee)的商品详情,您需要遵循以下步骤:注册并获取API权限首先,您需要在Shopee开放平台注册账号,并创建一个应用。在创建应用的过程中,您需要填写相关信息,包括应用名称、应用描述等。提交应用后,Shopee会审核您的应用并决定是否授予API调用权限。获取API凭证......
  • 线程池的使用场景
    在实际开发中,线程池用于优化线程的使用,提高系统性能,减少线程创建和销毁的开销,以及提供更高的系统稳定性。下面将详细解析几个常见的线程池使用场景,并结合源码和代码演示进行说明。场景一:Web应用的并发请求处理Web应用通常需要同时处理多个用户的请求。为了不每个请求都创建一......
  • 使用SSH命令连接到远程Linux服务器
    SSH是一种安全的加密协议,可用于远程登录和执行命令。无需额外安装客户端,并且适用于Linux、MacOS、Windows10以上的操作系统;基本命令要使用SSH连接到远程主机,请使用以下命令:sshuser@host其中:user是远程主机上的用户名。host是远程主机的IP地址或主机名。例如,要连接......