原文地址:优化你的应用结构和实现Redis缓存
项目地址:https://github.com/EDDYCJY/go...html
若是对你有所帮助,欢迎点个 Star 👍git
以前就在想,很多教程或示例的代码设计都是一步到位的(也没问题)github
但实际操做的读者真的可以理解透彻为何吗?冥思苦想,有了今天这一章的内容,我认为实际经历过一遍印象会更加深入golang
在本章节,将介绍如下功能的整理:redis
在规划阶段咱们发现了一个问题,这是目前的伪代码:shell
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), })
若是加上规划内的功能逻辑呢,伪代码会变成:json
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), })
若是缓存的逻辑也加进来,后面慢慢不断的迭代,岂不是会变成以下图同样?segmentfault
如今咱们发现了问题,应及时解决这个代码结构问题,同时把代码写的清晰、漂亮、易读易改也是一个重要指标api
在左耳朵耗子的文章中,这类代码被称为 “箭头型” 代码,有以下几个问题:缓存
一、个人显示器不够宽,箭头型代码缩进太狠了,须要我来回拉水平滚动条,这让我在读代码的时候,至关的不舒服
二、除了宽度外还有长度,有的代码的 if-else 里的 if-else 里的 if-else 的代码太多,读到中间你都不知道中间的代码是通过了什么样的层层检查才来到这里的
总而言之,“箭头型代码”若是嵌套太多,代码太长的话,会至关容易让维护代码的人(包括本身)迷失在代码中,由于看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,因此,箭头型代码是很是难以维护和Debug的。
简单的来讲,就是让出错的代码先返回,前面把全部的错误判断全判断掉,而后就剩下的就是正常的代码了
(注意:本段引用自耗子哥的 如何重构“箭头型”代码,建议细细品尝)
本项目将对既有代码进行优化和实现缓存,但愿你习得方法并对其余地方也进行优化
第一步:完成 Redis 的基础设施建设(须要你先装好 Redis)
第二步:对现有代码进行拆解、分层(不会贴上具体步骤的代码,但愿你可以实操一波,加深理解🤔)
打开 conf/app.ini 文件,新增配置:
... [redis] Host = 127.0.0.1:6379 Password = MaxIdle = 30 MaxActive = 30 IdleTimeout = 200
打开 pkg/e 目录,新建 cache.go,写入内容:
package e const ( CACHE_ARTICLE = "ARTICLE" CACHE_TAG = "TAG" )
(1)、打开 service 目录,新建 cache_service/article.go
写入内容:传送门
(2)、打开 service 目录,新建 cache_service/tag.go
写入内容:传送门
这一部分主要是编写获取缓存 KEY 的方法,直接参考传送门便可
打开 pkg 目录,新建 gredis/redis.go,写入内容:
package gredis import ( "encoding/json" "time" "github.com/gomodule/redigo/redis" "github.com/EDDYCJY/go-gin-example/pkg/setting" ) 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 != "" { if _, err := c.Do("AUTH", setting.RedisSetting.Password); 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 interface{}, time int) (bool, error) { conn := RedisConn.Get() defer conn.Close() value, err := json.Marshal(data) if err != nil { return false, err } reply, err := redis.Bool(conn.Do("SET", key, value)) conn.Do("EXPIRE", key, time) return reply, err } func Exists(key string) bool { conn := RedisConn.Get() defer conn.Close() exists, err := redis.Bool(conn.Do("EXISTS", key)) if err != nil { return false } return exists } func Get(key string) ([]byte, error) { conn := RedisConn.Get() defer conn.Close() reply, err := redis.Bytes(conn.Do("GET", key)) if err != nil { return nil, err } return reply, nil } func Delete(key string) (bool, error) { conn := RedisConn.Get() defer conn.Close() return redis.Bool(conn.Do("DEL", key)) } func LikeDeletes(key string) error { conn := RedisConn.Get() defer 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 }
在这里咱们作了一些基础功能封装
一、设置 RedisConn 为 redis.Pool(链接池)并配置了它的一些参数:
二、封装基础方法
文件内包含 Set、Exists、Get、Delete、LikeDeletes 用于支撑目前的业务逻辑,而在里面涉及到了如方法:
(1)RedisConn.Get()
:在链接池中获取一个活跃链接
(2)conn.Do(commandName string, args ...interface{})
:向 Redis 服务器发送命令并返回收到的答复
(3)redis.Bool(reply interface{}, err error)
:将命令返回转为布尔值
(4)redis.Bytes(reply interface{}, err error)
:将命令返回转为 Bytes
(5)redis.Strings(reply interface{}, err error)
:将命令返回转为 []string
在 redigo 中包含大量相似的方法,万变不离其宗,建议熟悉其使用规则和 Redis命令 便可
到这里为止,Redis 就能够愉快的调用啦。另外受篇幅限制,这块的深刻讲解会另外开设!
在先前规划中,引出几个方法去优化咱们的应用结构
要让错误提早返回,c.JSON 的侵入是不可避免的,可是可让其更具可变性,指不定哪天就变 XML 了呢?
一、打开 pkg 目录,新建 app/request.go,写入文件内容:
package app import ( "github.com/astaxie/beego/validation" "github.com/EDDYCJY/go-gin-example/pkg/logging" ) func MarkErrors(errors []*validation.Error) { for _, err := range errors { logging.Info(err.Key, err.Message) } return }
二、打开 pkg 目录,新建 app/response.go,写入文件内容:
package app import ( "github.com/gin-gonic/gin" "github.com/EDDYCJY/go-gin-example/pkg/e" ) type Gin struct { C *gin.Context } func (g *Gin) Response(httpCode, errCode int, data interface{}) { g.C.JSON(httpCode, gin.H{ "code": httpCode, "msg": e.GetMsg(errCode), "data": data, }) return }
这样子之后若是要变更,直接改动 app 包内的方法便可
打开 routers/api/v1/article.go,查看修改 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 } articleService := article_service.Article{ID: id} exists, err := articleService.ExistByID() if err != nil { appG.Response(http.StatusOK, e.ERROR_CHECK_EXIST_ARTICLE_FAIL, nil) return } if !exists { appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil) return } article, err := articleService.Get() if err != nil { appG.Response(http.StatusOK, e.ERROR_GET_ARTICLE_FAIL, nil) return } appG.Response(http.StatusOK, e.SUCCESS, article) }
这里有几个值得变更点,主要是在内部增长了错误返回,若是存在错误则直接返回。另外进行了分层,业务逻辑内聚到了 service 层中去,而 routers/api(controller)显著减轻,代码会更加的直观
例如 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 中,查找不到记录也算一种 “错误” 哦
显然,本章节并非你跟着我敲系列。我给你的课题是 “实现 Redis 缓存并优化既有的业务逻辑代码”
让其可以不断地适应业务的发展,让代码更清晰易读,且呈层级和结构性
若是有疑惑,能够到 go-gin-example 看看我是怎么写的,你是怎么写的,又分别有什么优点、劣势,取长补短一波?