Gin实践 连载十 优化你的应用结构和实现Redis缓存
优化你的应用结构和实现redis缓存
规划
在本章节,将介绍以下功能的整理:
- 抽离、分层业务逻辑:减轻 routers/*.go 内的 api方法的逻辑(但本文暂不分层 repository,这块逻辑还不重)
- 增加容错性:对 gorm 的错误进行判断
- Redis缓存:对获取数据类的接口增加缓存设置
- 减少重复冗余代码
问题在哪?
在规划阶段我们发现了一个问题,这是目前的伪代码:
if ! HasErrors() {
if ExistArticleByID(id) {
DeleteArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
logging.Info(err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
如果加上规划内的功能逻辑呢,伪代码会变成:
if ! HasErrors() {
exists, err := ExistArticleByID(id)
if err == nil {
if exists {
err = DeleteArticle(id)
if err == nil {
code = e.SUCCESS
} else {
code = e.ERROR_XXX
}
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
code = e.ERROR_XXX
}
} else {
for _, err := range valid.Errors {
logging.Info(err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": e.GetMsg(code),
"data": make(map[string]string),
})
如果缓存的逻辑也加进来,后面慢慢不断的迭代,岂不是会变成如下图一样?
现在我们发现了问题,应及时解决这个代码结构问题,同时把代码写的清晰、漂亮、易读易改也是一个重要指标
如何改?
在左耳朵耗子的文章中,这类代码被称为 “箭头型” 代码,有如下几个问题:
1、我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服
2、除了宽度外还有长度,有的代码的 if-else 里的 if-else 里的 if-else 的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的
总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的。
简单来说就是让出错的代码先返回,前面把所有的错误判断全判断掉,然后剩下的就是正常的代码了
落实
本项目将对既有代码进行优化和实现缓存,希望你习得方法并对其他地方也进行优化
第一步:完成 Redis 的基础设施建设(需要你先装好 Redis)
第二步:对现有代码进行拆解、分层(不会贴上具体步骤的代码,希望你能够实操一波,加深理解?)
Redis
一、配置
打开 conf/app.ini 文件,新增配置:
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
二、缓存Prefix
打开 pkg/e 目录,新建 cache.go,写入内容:
package e
const (
CACHE_ARTICLE = "ARTICLE"
CACHE_TAG = "TAG"
)
三、缓存key
(1)、打开 service 目录,新建 cache_service/article.go
点击查看代码
package cache_service
import (
"gin_log/pkg/e"
"strconv"
"strings"
)
type Article struct {
ID int
TagID int
State int
PageNum int
PageSize int
}
func (a *Article) GetArticleKey() string {
return e.CACHE_ARTICLE + "_" + strconv.Itoa(a.ID)
}
func (a *Article) GetArticlesKey() string {
keys := []string{e.CACHE_ARTICLE, "LIST"}
if a.ID > 0 {
keys = append(keys, strconv.Itoa(a.ID))
}
if a.TagID > 0 {
keys = append(keys, strconv.Itoa(a.TagID))
}
if a.State > 0 {
keys = append(keys, strconv.Itoa(a.State))
}
if a.PageNum > 0 {
keys = append(keys, strconv.Itoa(a.PageNum))
}
if a.PageSize > 0 {
keys = append(keys, strconv.Itoa(a.PageSize))
}
return strings.Join(keys, "_")
}
点击查看代码
package cache_service
import (
"gin_log/pkg/e"
"strconv"
"strings"
)
type Tag struct {
ID int
Name string
State int
PageNum int
PageSize int
}
func (t *Tag) GetTagsKey() string {
keys := []string{e.CACHE_TAG, "LIST"}
if t.Name != "" {
keys = append(keys, t.Name)
}
if t.State >= 0 {
keys = append(keys, strconv.Itoa(t.State))
}
if t.PageNum > 0 {
keys = append(keys, strconv.Itoa(t.PageNum))
}
if t.PageSize > 0 {
keys = append(keys, strconv.Itoa(t.PageSize))
}
return strings.Join(keys, "_")
}
点击查看代码
package gredis
import (
"encoding/json"
"gin_log/pkg/setting"
"github.com/gomodule/redigo/redis"
"time"
)
var redisConn *redis.Pool
func Setup() error {
redisConn = &redis.Pool{
MaxIdle: setting.RedisSetting.MaxIdle, // 最大空闲链接数
MaxActive: setting.RedisSetting.MaxActive, // 最大活跃链接数
IdleTimeout: setting.RedisSetting.IdleTimeout, // 在这段时间内保持空闲后关闭链接
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", setting.RedisSetting.Host)
if err != nil {
return nil, err
}
if setting.RedisSetting.Password != "" {
_, err = c.Do("AUTH", setting.RedisSetting.Password)
if err != nil {
_ = c.Close()
return nil, err
}
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
return nil
}
func Set(key string, data any, time int) error {
conn := redisConn.Get()
defer func() {
_ = conn.Close()
}()
value, err := json.Marshal(data)
if err != nil {
return err
}
_, err = conn.Do("SET", key, value)
if err != nil {
return err
}
_, err = conn.Do("EXPIRE", key)
if err != nil {
return err
}
return nil
}
func Exists(key string) bool {
conn := redisConn.Get()
defer func() {
_ = conn.Close()
}()
b, _ := redis.Bool(conn.Do("EXISTS", key))
return b
}
func Get(key string) ([]byte, error) {
conn := redisConn.Get()
defer func() {
_ = conn.Close()
}()
replay, err := redis.Bytes(conn.Do("GET", key))
if err != nil {
return nil, err
}
return replay, err
}
func Delete(key string) (bool, error) {
conn := redisConn.Get()
defer func() {
_ = conn.Close()
}()
return redis.Bool(conn.Do("DEL", key))
}
func LikeDeletes(key string) error {
conn := redisConn.Get()
defer func() {
_ = conn.Close()
}()
keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
if err != nil {
return err
}
for _, key := range keys {
_, err = Delete(key)
if err != nil {
return err
}
}
return nil
}
import (
"gin_log/pkg/logging"
"github.com/astaxie/beego/validation"
)
func MarkErrors(errors []*validation.Error) {
for _, err := range errors {
logging.Error(err.Key, err.Message)
}
return
}
2、打开 pkg 目录,新建 app/response.go,写入文件内容:
package app
import (
"gin_log/pkg/e"
"github.com/gin-gonic/gin"
)
type Gin struct {
C *gin.Context
}
func (g *Gin) Response(httpCode, errorCode int, data any) {
g.C.JSON(httpCode, gin.H{
"code": errorCode,
"msg": e.GetMsg(errorCode),
"data": data,
})
}
这样子以后如果要变动,直接改动 app 包内的方法即可
### 修改既有逻辑
打开 routers/api/v1/article.go,查看修改 GetArticle 方法后的代码为:
// GetArticle 获取单个文章
func GetArticle(c *gin.Context) {
appG := app.Gin{c}
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
if valid.HasErrors() {
app.MarkErrors(valid.Errors)
appG.Response(http.StatusOK, e.INVALID_PARAMS, nil)
return
}
if !models.ExistArticleByID(id) {
appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil)
}
article := models.GetArticle(id)
appG.Response(http.StatusOK, e.SUCCESS, gin.H{
"code": e.SUCCESS,
"msg": e.GetMsg(e.SUCCESS),
"data": article,
})
}
这里有几个值得变动点,主要是在内部增加了错误返回,如果存在错误则直接返回。
例如 service/article_service 下的 articleService.Get() 方法:
func (a Article) Get() (models.Article, error) {
var cacheArticle *models.Article
cache := cache_service.Article{ID: a.ID}
key := cache.GetArticleKey()
if gredis.Exists(key) {
data, err := gredis.Get(key)
if err != nil {
logging.Info(err)
} else {
json.Unmarshal(data, &cacheArticle)
return cacheArticle, nil
}
}
article, err := models.GetArticle(a.ID)
if err != nil {
return nil, err
}
gredis.Set(key, article, 3600)
return article, nil
}
而对于 gorm 的 错误返回设置,只需要修改 models/article.go 如下:
func GetArticle(id int) (*Article, error) {
var article Article
err := db.Where("id = ? AND deleted_on = ? ", id, 0).First(&article).Related(&article.Tag).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return &article, nil
}
习惯性增加 .Error,把控绝大部分的错误。另外需要注意一点,在 gorm 中,查找不到记录也算一种 “错误” 哦
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)