跟煎鱼学 Go
  • Introduction
  • 第1课 杂谈
    • 1.1 聊一聊,Go 的相对路径问题
    • 1.2 Go 的 fake-useragent 了解一下
    • 1.3 用 Go 来了解一下 Redis 通讯协议
    • 1.4 使用 Gomock 进行单元测试
    • 1.5 在 Go 中恰到好处的内存对齐
    • 1.6 来,控制一下 goroutine 的并发数量
    • 1.7 for-loop 与 json.Unmarshal 性能分析概要
    • 1.8 简单围观一下有趣的 //go: 指令
    • 1.9 我要在栈上。不,你应该在堆上
    • 1.10 defer 会有性能损耗,尽量不要用
    • 1.11 从实践到原理,带你参透 gRPC
    • 1.12 Go1.13 defer 的性能是如何提高的?
    • 1.13 Go 应用内存占用太多,让排查?(VSZ篇)
    • 1.14 干货满满的 Go Modules 和 goproxy.cn
  • 第2课 包管理
    • 2.1 Go依赖管理工具dep
    • 2.2 如此,用dep获取私有库
  • 第3课 gin
    • 3.1 Golang 介绍与环境安装
    • 3.2 Gin搭建Blog API's (一)
    • 3.3 Gin搭建Blog API's (二)
    • 3.4 Gin搭建Blog API's (三)
    • 3.5 使用JWT进行身份校验
    • 3.6 编写一个简单的文件日志
    • 3.7 优雅的重启服务
    • 3.8 为它加上Swagger
    • 3.9 将Golang应用部署到Docker
    • 3.10 定制 GORM Callbacks
    • 3.11 Cron定时任务
    • 3.12 优化配置结构及实现图片上传
    • 3.13 优化你的应用结构和实现Redis缓存
    • 3.14 实现导出、导入 Excel
    • 3.15 生成二维码、合并海报
    • 3.16 在图片上绘制文字
    • 3.17 用Nginx部署Go应用
    • 3.18 Golang交叉编译
    • 3.19 请入门 Makefile
  • 第4课 grpc
    • 4.1 gRPC及相关介绍
    • 4.2 gRPC Client and Server
    • 4.3 gRPC Streaming, Client and Server
    • 4.4 TLS 证书认证
    • 4.5 基于 CA 的 TLS 证书认证
    • 4.6 Unary and Stream interceptor
    • 4.7 让你的服务同时提供 HTTP 接口
    • 4.8 对 RPC 方法做自定义认证
    • 4.9 gRPC Deadlines
    • 4.10 分布式链路追踪
  • 第5课 grpc-gateway
    • 5.1 介绍与环境安装
    • 5.2 Hello World
    • 5.3 Swagger了解一下
    • 5.4 能不能不用证书?
  • 第6课 常用关键字
    • 6.1 panic and recover
    • 6.2 defer
  • 第7课 数据结构
    • 7.1 slice
    • 7.2 slice:最大容量大小是怎么来的
    • 7.3 map:初始化和访问元素
    • 7.4 map:赋值和扩容迁移
    • 7.5 map:为什么遍历 map 是无序的
  • 第8课 标准库
    • 8.1 fmt
    • 8.2 log
    • 8.3 unsafe
  • 第9课 工具
    • 9.1 Go 大杀器之性能剖析 PProf
    • 9.2 Go 大杀器之跟踪剖析 trace
    • 9.3 用 GODEBUG 看调度跟踪
    • 9.4 用 GODEBUG 看GC
  • 第10课 爬虫
    • 9.1 爬取豆瓣电影 Top250
    • 9.2 爬取汽车之家 二手车产品库
    • 9.3 了解一下Golang的市场行情
Powered by GitBook
On this page
  • 前言
  • 本文目标
  • 问题在哪?
  • 如何改?
  • 落实
  • Redis
  • 拆解、分层
  • 最后
  • 参考
  • 本系列示例代码
  • 推荐阅读
  • 关于
  • 修改记录
  • ?
  • 我的公众号

Was this helpful?

  1. 第3课 gin

3.13 优化你的应用结构和实现Redis缓存

Previous3.12 优化配置结构及实现图片上传Next3.14 实现导出、导入 Excel

Last updated 5 years ago

Was this helpful?

项目地址:

前言

之前就在想,不少教程或示例的代码设计都是一步到位的(也没问题)

但实际操作的读者真的能够理解透彻为什么吗?左思右想,有了今天这一章的内容,我认为实际经历过一遍印象会更加深刻

本文目标

在本章节,将介绍以下功能的整理:

  • 抽离、分层业务逻辑:减轻 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

(2)、打开 service 目录,新建 cache_service/tag.go

这一部分主要是编写获取缓存 KEY 的方法,直接参考传送门即可

四、Redis 工具包

打开 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) error {
    conn := RedisConn.Get()
    defer 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, time)
    if err != nil {
        return err
    }

    return nil
}

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
}

在这里我们做了一些基础功能封装

1、设置 RedisConn 为 redis.Pool(连接池)并配置了它的一些参数:

  • Dial:提供创建和配置应用程序连接的一个函数

  • TestOnBorrow:可选的应用程序检查健康功能

  • MaxIdle:最大空闲连接数

  • MaxActive:在给定时间内,允许分配的最大连接数(当为零时,没有限制)

  • IdleTimeout:在给定时间内将会保持空闲状态,若到达时间限制则关闭连接(当为零时,没有限制)

2、封装基础方法

文件内包含 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

到这里为止,Redis 就可以愉快的调用啦。另外受篇幅限制,这块的深入讲解会另外开设!

拆解、分层

在先前规划中,引出几个方法去优化我们的应用结构

  • 错误提前返回

  • 统一返回方法

  • 抽离 Service,减轻 routers/api 的逻辑,进行分层

  • 增加 gorm 错误判断,让错误提示更明确(增加内部错误码)

编写返回方法

要让错误提前返回,c.JSON 的侵入是不可避免的,但是可以让其更具可变性,指不定哪天就变 XML 了呢?

1、打开 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
}

2、打开 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": errCode,
        "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 缓存并优化既有的业务逻辑代码”

让其能够不断地适应业务的发展,让代码更清晰易读,且呈层级和结构性

参考

本系列示例代码

推荐阅读

关于

修改记录

  • 第一版:2018年02月16日发布文章

  • 第二版:2019年10月01日修改文章

?

我的公众号

image

(注意:本段引用自耗子哥的 ,建议细细品尝)

写入内容:

写入内容:

在 中包含大量类似的方法,万变不离其宗,建议熟悉其使用规则和 即可

如果有疑惑,可以到 看看我是怎么写的,你是怎么写的,又分别有什么优势、劣势,取长补短一波?

如果有任何疑问或错误,欢迎在 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

image
https://github.com/EDDYCJY/go-gin-example
如何重构“箭头型”代码
传送门
传送门
redigo
Redis命令
go-gin-example
go-gin-example
如何重构“箭头型”代码
issues