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

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

前言

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

总结

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


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

本文来自:简书

感谢作者:_老七

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

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

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