随着项目的庞大,传统的全局变量模式越来越力不从心了
我们需要引入依赖注入
准备工作
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
}
1309

被折叠的 条评论
为什么被折叠?



