分享
  1. 首页
  2. 文章

golang通过结构体的继承、重写封装的一个高复用的公用查询

_老七 · · 2830 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

在golang开发中会发现,没有泛型会写大量重复代码,例如:对数据库表分页查询时,大多情况是表名不同,查询条件与查询字段不同,正常情况下,就得写多份重叠代码。本文主要是对过结构体继承(其实是组合),模拟泛型(用interface类型),来封装业务层的公用查询逻辑。
其中会用到gorm查询时不固定定条件查询,可以看我另一篇博文go语言对gorm不固定条件查询封装

思路

  1. 既然要公用,那就得定义一个baseservice.go文件,别的业务继承basebaseservice.go
  2. 继承后,如何重写父结构体的方法,以及父结构体的方法如何调用子结构体的方法,来实现高复用性
  3. 因为不支持泛型,在gorm查询时,所需结构体就得用interface,试想gorm本身就是一个公用查询框架,传入interface,然后用reflect反射等到数据。

代码

  • 父结构体及业务逻辑:baseservice.go
package services
import (
 "encoding/json"
 "github.com/go-redis/redis/v7"
 "github.com/jinzhu/gorm"
 "math"
 "reflect"
 "strconv"
 "time"
 "weichai/app/cache"
 "weichai/app/models/entity"
 "weichai/pkg/utils"
)
type BaseService struct {
 // 要操作的model结构体[必须为指针类型的结构体*slice]
 Model interface{} //model必须是指针
 CachePrefix string //缓存的前缀
 // 不同的业务,有不同的库查询逻辑,所以抽象此方法,让子结构体来实现。默认方法 queryList
 QueryList func(wheres interface{}, columns interface{}, orderBy interface{}, page, rows int, total *int) (list interface{}, err error)
}
// model必须是指针
func NewBaseService(model interface{}, cachePrefix string) *BaseService {
 bs := &BaseService{}
 bs.Model = model
 bs.CachePrefix = cachePrefix
 // 赋值默认方法
 bs.QueryList = bs.queryList
 return bs
}
// 根据id返回数据
// 返回值 nil|*struct{} ,当err不为nil|没有找到记录时,返回值=nil
func (service *BaseService) GetById(id int) (interface{}, error) {
 db := entity.DB
 model := service.GetNewModel()
 err := db.Where("id = ?", id).First(model, id).Error
 if err != nil && err != gorm.ErrRecordNotFound {
 return nil, err
 } else if err == gorm.ErrRecordNotFound {
 return nil, nil
 }
 return model, nil
}
// 根据查询条件返回列表数据[会走缓存]
// 返回值 list:nil|*[]*struct{} ,当err不为nil时,list=nil
func (service *BaseService) List(wheres interface{}, columns interface{}, orderBy interface{}, page, rows int, total *int) (list interface{}, err error) {
 vb, err := json.Marshal([]interface{}{wheres, columns, orderBy, page, rows})
 if err != nil {
 return nil, err
 }
 pkey := utils.GetMd5String(string(vb))
 prefix := service.CachePrefix
 ckey := prefix + "_list:" + pkey
 ckey_total := prefix + "_list_total:" + pkey
 _total, err := cache.Get(ckey_total)
 if err == nil {
 _t, err := strconv.Atoi(_total)
 if err == nil {
 *total = _t
 if math.Ceil(float64(_t/rows)) < float64(page) {
 list = service.GetNewModelSlice()
 return list, nil
 }
 }
 }
 data, err := cache.Get(ckey)
 is_cache_data := false
 if err == redis.Nil || err != nil {
 // 防止缓存穿透,需要加锁 【只有ckey相同时,才会互斥锁】
 lock := utils.MultipleMutex.Lock(ckey)
 data, err = cache.Get(ckey)
 if err == redis.Nil || err != nil {
 if service.QueryList == nil {
 service.QueryList = service.queryList
 }
 list, err = service.QueryList(wheres, columns, orderBy, page, rows, total)
 if err == nil {
 exp := time.Second * 30 //在实际开发中,可以把过期时间放到结构体中,让子结构体赋值
 _, _ = cache.Set(ckey, list, exp)
 _, _ = cache.Set(ckey_total, total, exp)
 //set出错,上报
 } else {
 utils.MultipleMutex.Unlock(lock)
 return nil, err
 }
 } else {
 is_cache_data = true
 }
 utils.MultipleMutex.Unlock(lock)
 } else {
 is_cache_data = true
 }
 if is_cache_data {
 list = service.GetNewModelSlice()
 err := json.Unmarshal(([]byte)(data), list)
 if err != nil {
 return nil, err
 }
 }
 return list, err
}
// 根据查询条件返回列表数据[直接查库]
// 返回值 list:nil|*[]*struct{} ,当err不为nil时,list=nil
func (service *BaseService) queryList(wheres interface{}, columns interface{}, orderBy interface{}, page, rows int, total *int) (list interface{}, err error) {
 db := entity.DB
 list = service.GetNewModelSlice()
 db, err = entity.BuildQueryList(db, wheres, columns, orderBy, page, rows)
 if err != nil {
 return nil, err
 }
 err = db.Find(list).Error
 if err != nil {
 return nil, err
 }
 db = entity.DB
 db, err = entity.BuildWhere(db, wheres)
 if err != nil {
 return nil, err
 }
 db.Model(service.GetNewModel()).Count(total)
 return list, nil
}
// 获取新的struct,返回值 *struct{}
func (service *BaseService) GetNewModel() interface{} {
 t := reflect.TypeOf(service.Model)
 m := t.Elem()
 return reflect.Indirect(reflect.New(m)).Addr().Interface()
}
// 获取新的struct切片,返回值 *[]*struct{}
func (service *BaseService) GetNewModelSlice() interface{} {
 t := reflect.TypeOf(service.Model)
 // return reflect.Indirect(reflect.New(reflect.SliceOf(t))).Addr().Interface()
 list := reflect.New(reflect.SliceOf(t)).Elem()
 list.Set(reflect.MakeSlice(list.Type(), 0, 0))
 return reflect.Indirect(list).Addr().Interface()
}

代码说明:

主要实现功能:定义BaseService结构体,抽象出QueryList方法[子结构体实现抽象方法],List分页查询方法[包含redis缓存],反射出gorm查询数据时所需结构体

  1. BaseService 结构体:创建一个BaseService结构体系,Model属性是 在gorm查询时所用到的结构体,这里必须为指针类型的结构体*slice,因应后面反射结构体时用到。
    (1). QueryList方法:此方法是对外提供的一个方法,方便子结构实现不同业务查询。queryList方法是一个公用库查询方法,只是简单的做单表查询,并不包含关联查询,如果有关联查询、预加载或特殊业务逻辑,子结构体就要单独实现
  2. List 方法:公用分页查询方法,这个方法会走缓存。
    (1). cache:基于go-redis封装的缓存包,后面会贴出代码
    (2). utils.MultipleMutex:封装的多个互斥锁,只有key相同时,才会互斥锁,后面会贴出代码
  3. queryList 方法:数据库查询逻辑。方法的参数,entity.DB,与entity.BuildQueryList方法,具体请看go语言对gorm不固定条件查询封装这篇博文。
  4. GetNewModel 方法:根据BaseService里的Model属性,反射生成一个新的结构体,返回的是个指针
  5. GetNewModelSlice 方法:根据BaseService里的Model属性,反射生成一个新的切片结构体,返回的是切片指针
    说明:baseservice.go只实现了部分公用方法,在实际开发中,公用逻辑远比这多,可以根据自己业务需求来做相应的封装
  • 子结构体:user.go
package user
import (
 "github.com/jinzhu/gorm"
 "weichai/app/models/entity"
 userModel "weichai/app/models/user"
 "weichai/app/services"
)
type userService struct {
 *services.BaseService // 组合BaseService结构体,实现继承
}
// 必须是指针 &userModel.User{}
var _bs = services.NewBaseService(&userModel.User{}, "user")
var _us = &userService{BaseService: _bs}
func NewUserService() *userService {
 // 给父结构体的QueryList方法赋值,来达到重写需求【由于golang不是OOP,所以'重写'也不能达到重写,父结构体是没有办法直接调用重写的方法的,所以要通过在结构体中定义方法,子结构体给它赋值】
 _bs.QueryList = _us.QueryList
 return _us
}
/*// 重写(覆盖)父结构体的List方法,来实现特殊需求
func (service *userService) List(wheres interface{}, columns interface{}, orderBy interface{}, page, rows int, total *int) (list interface{}, err error) {
 // 调用父的List: service.BaseService.List()
 return nil, nil
}*/
// 实现抽象方法,来实现特殊业务,此方法包含了 关联查询的预加载逻辑
func (service *userService) QueryList(wheres interface{}, columns interface{}, orderBy interface{}, page, rows int, total *int) (list interface{}, err error) {
 db := entity.DB
 var model []*userModel.User
 var mod userModel.User
 db, err = entity.BuildQueryList(db, wheres, columns, orderBy, page, rows)
 if err != nil {
 return nil, err
 }
 err = db.Preload("UserCard", func(db *gorm.DB) *gorm.DB {
 return db.Order("created_at asc")
 }).Find(&model).Error
 db = entity.DB
 db, err = entity.BuildWhere(db, wheres)
 if err != nil {
 return nil, err
 }
 db.Model(&mod).Count(total)
 return &model, nil
}
  • 在controller里查询用户列表
func List(ctx *gin.Context) {
 total := 0
 where := []interface{}{
 []interface{}{"id", "in", []int{1, 2}},
 }
 //var wg sync.WaitGroup
 //wg.Add(2)
 //测试多协程查询时加锁
 /*go func() {
 bll := userService.NewUserService()
 list, _ := bll.BaseService.List(where, []string{"*"}, "id desc", 1, 1, &total)
 list = list.(*[]*user.User)
 wg.Done()
 }()
 go func() {
 bll := userService.NewUserService()
 _, _ = bll.BaseService.List(where, []string{"*"}, "id desc", 1, 1, &total)
 wg.Done()
 }()*/
 bll := userService.NewUserService()
 res, _ := bll.List(where, []string{"*"}, "id desc", 1, 1, &total)
 list := res.(*[]*user.User)
 //wg.Wait()
 ctx.JSON(http.StatusOK, utils.Result(result.OK, map[string]interface{}{
 "list": list, "total": total,
 }, ""))
}

其它代码

  • utils.MultipleMutex所用到代码文件

逻辑也比较简单,根据相同的key返回sync.Mutex的指针,并存储在map里;在Unlock时,删除map的值
不同的key会返回不同的sync.Mutex,所以在应用时不会锁住资源,达到并发需求

package utils
import (
 "sync"
)
var MultipleMutex = &multipleMutex{
 keys: map[string]*lock{},
 keyMutex: &sync.Mutex{},
}
type multipleMutex struct {
 keys map[string]*lock
 keyMutex *sync.Mutex
}
type lock struct {
 key *string
 mutex *sync.Mutex
}
func (mm *multipleMutex) Lock(key string) *lock {
 mm.keyMutex.Lock()
 mutex, ok := mm.keys[key]
 if !ok {
 mutex = &lock{
 key: &key,
 mutex: new(sync.Mutex),
 }
 mm.keys[key] = mutex
 }
 mm.keyMutex.Unlock()
 mutex.mutex.Lock()
 return mutex
}
func (mm *multipleMutex) Unlock(lock *lock) {
 key := lock.key
 mm.keyMutex.Lock()
 mutex, ok := mm.keys[*key]
 if ok && mutex == lock {
 // 删除map的key,如果有引用lock,是不会触发GC的,所以别的协程执行后面的lock.mutex.Unlock()不会有问题
 delete(mm.keys, *key)
 }
 mm.keyMutex.Unlock()
 lock.mutex.Unlock()
}
  • cache用到的代码文件
package cache
import (
 "encoding/json"
 "reflect"
 "time"
 "weichai/pkg/redis"
)
func Set(key string, val interface{}, expire time.Duration) (ok bool, err error) {
 kind := reflect.TypeOf(val).Kind()
 var v interface{}
 switch kind {
 case reflect.Interface, reflect.Map, reflect.Slice, reflect.Struct, reflect.Array, reflect.Ptr:
 vb, err := json.Marshal(val)
 if err != nil {
 return false, err
 }
 v = string(vb)
 default:
 v = val
 }
 res, err := redis.RedisClient.Set(redis.CreateKey(key), v, expire).Result()
 return res == "OK", err
}
func Get(key string) (val string, err error) {
 return redis.RedisClient.Get(redis.CreateKey(key)).Result()
}
// redis用的是 github.com/go-redis/redis/v7,redis.CreateKey(key)返回一个加了前缀的key。这些代码就不贴了

总结

代码模拟了结构体的继承,重写,抽象方法来实现一个高复用的公用查询逻辑,在开发过程中能节省不少的代码量,使代码更整洁


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2830 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏