前言

泛型

思路

baseservice.gobasebaseservice.gointerface

代码

  • 父结构体及业务逻辑: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()
}

代码说明:

BaseServiceQueryListListgorm
BaseService必须为指针类型的结构体*slice参数entity.DBentity.BuildQueryList说明: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。这些代码就不贴了

总结

继承重写抽象方法