前言
在golang开发中会发现,没有
泛型
会写大量重复代码,例如:对数据库表分页查询时,大多情况是表名不同,查询条件与查询字段不同,正常情况下,就得写多份重叠代码。本文主要是对过结构体继承(其实是组合),模拟泛型(用interface类型),来封装业务层的公用查询逻辑。
其中会用到gorm查询时不固定定条件查询,可以看我另一篇博文go语言对gorm不固定条件查询封装
思路
- 既然要公用,那就得定义一个
baseservice.go
文件,别的业务继承basebaseservice.go
- 继承后,如何重写父结构体的方法,以及父结构体的方法如何调用子结构体的方法,来实现高复用性
- 因为不支持泛型,在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
查询数据时所需结构体
- BaseService 结构体:创建一个
BaseService
结构体系,Model属性是 在gorm查询时所用到的结构体,这里必须为指针类型的结构体*slice
,因应后面反射结构体时用到。
(1). QueryList方法:此方法是对外提供的一个方法,方便子结构实现不同业务查询。queryList方法是一个公用库查询方法,只是简单的做单表查询,并不包含关联查询,如果有关联查询、预加载或特殊业务逻辑,子结构体就要单独实现 - List 方法:公用分页查询方法,这个方法会走缓存。
(1). cache:基于go-redis封装的缓存包,后面会贴出代码
(2). utils.MultipleMutex:封装的多个互斥锁,只有key相同时,才会互斥锁,后面会贴出代码 - queryList 方法:数据库查询逻辑。方法的
参数
,entity.DB
,与entity.BuildQueryList
方法,具体请看go语言对gorm不固定条件查询封装这篇博文。 - GetNewModel 方法:根据BaseService里的Model属性,反射生成一个新的结构体,返回的是个指针
- 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。这些代码就不贴了
总结
代码模拟了结构体的继承
,重写
,抽象方法
来实现一个高复用的公用查询逻辑,在开发过程中能节省不少的代码量,使代码更整洁
有疑问加站长微信联系(非本文作者)