go wire自动注入框架

随着项目的庞大,传统的全局变量模式越来越力不从心了

我们需要引入依赖注入

准备工作

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get github.com/glebarez/sqlite
go get go.uber.org/zap
go get github.com/google/wire/cmd/wire@latest

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


本课程使用gin+gorm传统项目的模式进行讲解

传统全局变量模式

在go语言中,很多项目很喜欢使用全局变量的模式

全局变量的思想就是将实例放到全局,其他任何地方都可以使用

项目结构

api                      // control层,与前端交互的
  user_api
    enter.go
  enter.go
core                     // 初始化,连接外部的,比如数据库、日志、redis等
  db.go
  log.go
global                   // 全局变量
  enter.go
models                   // 表
  user_model.go
router                   // 路由
  enter.go
  user_router.go         // 每个control都有单独的路由文件
service                  // 服务层     
  user_service           // 每个服务都有专门的一个包
    enter.go
main.go                 // 主入口

全局变量模式下,在main函数中初始化全局变量

package main

import (
  "go-wire/core"
  "go-wire/global"
  "go-wire/router"
)

func main() {
  global.DB = core.NewDB()
  global.LOG = core.NewLog()

  router.Run()
}

在control层,主要与前端交互、参数校验,调用service层的方法,组装数据

type UserApi struct {
}

type GetUserRequest struct {
  ID uint `form:"id" binding:"required"`
}

func (h *UserApi) GetUserView(c *gin.Context) {
  var cr GetUserRequest
  err := c.ShouldBindQuery(&cr)
  if err != nil {
    c.JSON(http.StatusOK, gin.H{"code": -1, "msg": "参数绑定失败"})
    return
  }
  user := user_service.UserService{}
  userModel, err := user.GetUser(cr.ID)
  if err != nil {
    c.JSON(http.StatusOK, gin.H{"code": 2, "msg": "用户不存在"})
    return
  }
  c.JSON(http.StatusOK, userModel)
}

在service层中,主要是对后端数据进行交互,比如查询数据库,查询外部数据等

type UserService struct {
}

func (UserService) GetUser(id uint) (user *models.UserModel, err error) {
  err = global.DB.Take(&user, id).Error
  if err != nil {
    global.LOG.Sugar().Warnf("不存在的用户 %d", id)
  }
  return
}

传统全局变量有何问题?

传统的全局变量模式在功能上没有任何问题,特别是对于一些小型项目,这种结构反而更方便

但是项目庞大起来、负责参与的人多了之后,就会有一些明显的问题

  • service 层直接依赖全局变量,耦合度高
  • 依赖关系隐藏在代码中,不直观

什么是依赖关系

比如在userService中,需要借助db查询数据库,在没有查到的时候记录一下日志

也就是说在userService的GetUser方法中,依赖db和log

如果在main中,没有初始化,在运行时,程序走到这里就会报错

有人可能会有疑问,代码都是我写的,有没有依赖我还不清楚吗

但是问题是很多时候代码是团队协作开发的,这个全局变量在哪里被赋值、哪里有改过、你可能还真不一定清楚,很多时候都是出了bug才排查到这里来,并且就算是自己写的,时间长了之后,自己也是会忘的

手动依赖注入

为解决全局变量的问题,我们先尝试手动传递依赖

主要是使用构造函数,将自己需要的依赖显式传递进来

对于userService来说,它需要依赖db和log

package user_service

import (
  "go-wire/models"
  "go.uber.org/zap"
  "gorm.io/gorm"
)

type UserService struct {
  db *gorm.DB
  log *zap.Logger
}

func NewUserService(db *gorm.DB, log *zap.Logger) *UserService  {
  return &UserService{
    db: db,
    log: log,
  }
}

func (u *UserService) GetUser(id uint) (user *models.UserModel, err error) {
  err = u.db.Take(&user, id).Error
  if err != nil {
    u.log.Sugar().Warnf("不存在的用户 %d", id)
  }
  return
}

对于user的control来说,它需要依赖userService

并且为了方便,可以把路由也放到这里来

package user_api

import (
  "github.com/gin-gonic/gin"
  "go-wire/service/user_service"
)

type UserApi struct {
  userService *user_service.UserService
}

func NewUserApi(engine *gin.Engine, service *user_service.UserService) *UserApi {
  userApi := &UserApi{
    userService: service,
  }

  g := engine.Group("api")
  g.GET("user", userApi.GetUserView)
  return userApi
}


某一个视图函数

package user_api

import (
  "github.com/gin-gonic/gin"
  "net/http"
)

type GetUserRequest struct {
  ID uint `form:"id" binding:"required"`
}

func (u *UserApi) GetUserView(c *gin.Context) {
  var cr GetUserRequest
  err := c.ShouldBindQuery(&cr)
  if err != nil {
    c.JSON(http.StatusOK, gin.H{"code": -1, "msg": "参数绑定失败"})
    return
  }
  userModel, err := u.userService.GetUser(cr.ID)
  if err != nil {
    c.JSON(http.StatusOK, gin.H{"code": 2, "msg": "用户不存在"})
    return
  }
  c.JSON(http.StatusOK, userModel)
}

这种模式下,就不在需要最外层的api了,并且不在需要单独的api路由了

对于router来说,就承担了整个的入口

依赖的项目就很多了

package router

import (
  "github.com/gin-gonic/gin"
  "go-wire/api/user_api"
  "go-wire/core"
  "go-wire/service/user_service"
)

func NewGin() *gin.Engine {
  r := gin.Default()
  // 可以在这里加载中间件
  return r
}

func Run() {
  engine := NewGin()
  db := core.NewDB()
  log := core.NewLog()
  service := user_service.NewUserService(db, log)
  user_api.NewUserApi(engine, service)

  engine.Run(":8080")
}


在main函数中,就很简单了

package main

import (
  "go-wire/router"
)

func main() {
  router.Run()
}

这样,就解决了全局变量的问题了

但是也引入了新的问题

  • 当依赖链变长(如 A→B→C→D),手动传递代码会变得繁琐
  • 依赖顺序需要手动维护,容易出错

依赖注入

可以通过参数的类型自动分析,哪个结构体需要什么参数

wire就是做这个事情的,我们先从简单的开始

wire.Build

type UserService struct {
  db  *gorm.DB
  log *zap.Logger
}

func NewUserService(db *gorm.DB, log *zap.Logger) *UserService {
  return &UserService{
    db:  db,
    log: log,
  }
}

func (u *UserService) GetUser(id uint) (user *models.UserModel, err error) {
  err = u.db.Take(&user, id).Error
  if err != nil {
    u.log.Sugar().Warnf("不存在的用户 %d", id)
  }
  return
}

这个示例比较简单,一个UserService,依赖db和log,并且它有一个方法

那么我们正常处理依赖应该是这样的

func InitializeUserService() *UserService {
  db := core.NewDB()
  log := core.NewLog()
  us := NewUserService(db, log)
  return us
}

那么在wire眼里,我们需要这样做

func InitializeUserService() *UserService {
  wire.Build(
    NewUserService,
    core.NewDB,
    core.NewLog,
  )
  return nil
}

那就把UserService需要的依赖,直接一股脑的放到build里面就好了

然后在build这个文件的目录,执行 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

import (
  "go-wire/core"
)

// Injectors from wire1_xx.go:

func InitializeUserService() *UserService {
  db := core.NewDB()
  logger := core.NewLog()
  userService := NewUserService(db, logger)
  return userService
}

可以把 //go:build wireinject 加到文件的最上面,不然编译的时候会报错

但是很麻烦的就是,加了之后你再想改就没有代码提示了,这一点我感觉很傻

当然,InitializeXXX构造函数也是支持自定义多响应和自定义参数的


func InitializeUserService() (*UserService, error) {
  wire.Build(
    NewUserService,
    core.NewDB,
    core.NewLog,
  )
  return nil, nil
}

如果其中有构造函数判断了err,生成的代码也会进行err判断,如下

// Injectors from wire1_xx.go:

func InitializeUserService() (*UserService, error) {
  db := core.NewDB()
  logger := core.NewLog()
  userService, err := NewUserService(db, logger)
  if err != nil {
    return nil, err
  }
  return userService, nil
}

也能支持传参数进去

func InitializeUserService(db *gorm.DB) (*UserService, error) {
  wire.Build(
    NewUserService,
    core.NewLog,
  )
  return nil, nil
}

生成之后的

func InitializeUserService(db *gorm.DB) (*UserService, error) {
  logger := core.NewLog()
  userService, err := NewUserService(db, logger)
  if err != nil {
    return nil, err
  }
  return userService, nil
}

wire.NewSet

当如果有多个服务都需要初始化,那么可以在每个服务里面编写wire.NewSet

var UserProvider = wire.NewSet(NewUserService, core.NewDB, core.NewLog)

然后在build里面直接使用这个provider即可

func InitializeUserService() *UserService {
  wire.Build(
    UserProvider,
  )
  return nil
}

wire.Struct

之前我们生成都是以构造函数的形式生成的

其实也可以直接以结构体字段赋值的形式生成

func InitializeUserService(db *gorm.DB) (*UserService, error) {
  wire.Build(
    wire.Struct(new(UserService), "db", "log"),
    core.NewLog,
  )
  return nil, nil
}

生成的代码如下

// Injectors from wire1_xx.go:

func InitializeUserService(db *gorm.DB) (*UserService, error) {
  logger := core.NewLog()
  userService := &UserService{
    db:  db,
    log: logger,
  }
  return userService, nil
}


如果要给每个字段赋值,可以直接使用*

func InitializeUserService(db *gorm.DB) (*UserService, error) {
  wire.Build(
    wire.Struct(new(UserService), "*"),
    core.NewLog,
  )
  return nil, nil
}

wire ./... 可以生成当前目录以及当前目录所有子目录和文件的Build

wire.Value

如果需要传字面量,可以使用wire.Value

type Info struct {
  Name string
}

func InitializeInfo() *Info {
  wire.Build(
    wire.Value(&Info{Name: "name"}),
  )
  return nil
}

生成代码如下

func InitializeInfo() *Info {
  info := _wireInfoValue
  return info
}

var (
  _wireInfoValue = &Info{Name: "name"}
)

wire.FieldsOf

可以生成GetXX的函数,调用这个函数可以获取这个结构体的字段值

type Info struct {
  Name string
}

func NewInfo() *Info {
  return &Info{Name: "xxx"}
}
func GetName() string {
  wire.Build(
    NewInfo,
    wire.FieldsOf(new(*Info), "Name"))
  return ""
}

生成代码如下

// Injectors from wire1_xx.go:

func GetName() string {
  info := NewInfo()
  string2 := info.Name
  return string2
}

// wire1_xx.go:

type Info struct {
  Name string
}

func NewInfo() *Info {
  return &Info{Name: "xxx"}
}

wire.Bind

对接口类型进行绑定

type Info struct {
  Name string
}

func (i Info) GetName() string {
  return i.Name
}

func NewInfo() *Info {
  return &Info{Name: "xxx"}
}

type InfoInterface interface {
  GetName() string
}

func InitializeInfo() InfoInterface {
  wire.Build(
    NewInfo,
    wire.Bind(new(InfoInterface), new(*Info)),
  )
  return nil
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值