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

前言

在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。这些代码就不贴了

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,941评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,397评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,345评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,851评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,868评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,688评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,414评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,319评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,775评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,945评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,096评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,789评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,437评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,993评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,107评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,308评论 3 372
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,037评论 2 355

推荐阅读更多精彩内容

  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类...
    司马DE晴空阅读 1,295评论 0 7
  • 此刻的我本应该在上班,缺因为身体原因躺在家里。阳光透过窗帘,掉在桌上。家里乱糟糟的,好吧,好还有心情来记录两...
    吉米读写阅读 354评论 0 0
  • 今天微博热搜榜上出现了这样一个话题 点开进去,讲一个奶奶把两个孙女赶出家门,民警发现了在街上游荡的姐妹俩,把她们送...
    zippo的玫瑰恋人阅读 273评论 0 1
  • 友情,亲情,爱情,生命真的无价么?不知道如何回答的时候,试着看看下面的几个问题。 有多少朋友是因为金钱的关系闹掰的...
    haolujun阅读 698评论 2 2
  • 郭相麟 贵州好久没有在二月份下雪了,早上推开窗户,只见窗外白茫茫的一片,空中飘着鹅毛大雪,街边的灯笼挂着雪花显得格...
    郭相麟阅读 217评论 0 0