Golang Gin實踐 連載十三 優化你的應用結構和實現Redis緩存

原文地址:優化你的應用結構和實現Redis緩存
項目地址:https://github.com/EDDYCJY/go...html

若是對你有所幫助,歡迎點個 Star 👍git

前言

以前就在想,很多教程或示例的代碼設計都是一步到位的(也沒問題)github

但實際操做的讀者真的可以理解透徹爲何嗎?冥思苦想,有了今天這一章的內容,我認爲實際經歷過一遍印象會更加深入golang

規劃

在本章節,將介紹如下功能的整理:redis

  • 抽離、分層業務邏輯:減輕 routers/*.go 內的 api方法的邏輯(但本文暫不分層 repository,這塊邏輯還不重)
  • 增長容錯性:對 gorm 的錯誤進行判斷
  • 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

image

如今咱們發現了問題,應及時解決這個代碼結構問題,同時把代碼寫的清晰、漂亮、易讀易改也是一個重要指標api

如何改?

在左耳朵耗子的文章中,這類代碼被稱爲 「箭頭型」 代碼,有以下幾個問題:緩存

一、個人顯示器不夠寬,箭頭型代碼縮進太狠了,須要我來回拉水平滾動條,這讓我在讀代碼的時候,至關的不舒服

二、除了寬度外還有長度,有的代碼的 if-else 裏的 if-else 裏的 if-else 的代碼太多,讀到中間你都不知道中間的代碼是通過了什麼樣的層層檢查纔來到這裏的

總而言之,「箭頭型代碼」若是嵌套太多,代碼太長的話,會至關容易讓維護代碼的人(包括本身)迷失在代碼中,由於看到最內層的代碼時,你已經不知道前面的那一層一層的條件判斷是什麼樣的,代碼是怎麼運行到這裏的,因此,箭頭型代碼是很是難以維護和Debug的。

簡單的來講,就是讓出錯的代碼先返回,前面把全部的錯誤判斷全判斷掉,而後就剩下的就是正常的代碼了

(注意:本段引用自耗子哥的 如何重構「箭頭型」代碼,建議細細品嚐)

落實

本項目將對既有代碼進行優化和實現緩存,但願你習得方法並對其餘地方也進行優化

第一步:完成 Redis 的基礎設施建設(須要你先裝好 Redis)

第二步:對現有代碼進行拆解、分層(不會貼上具體步驟的代碼,但願你可以實操一波,加深理解🤔)

Redis

1、配置

打開 conf/app.ini 文件,新增配置:

...
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200

2、緩存 Prefix

打開 pkg/e 目錄,新建 cache.go,寫入內容:

package e

const (
    CACHE_ARTICLE = "ARTICLE"
    CACHE_TAG     = "TAG"
)

3、緩存 Key

(1)、打開 service 目錄,新建 cache_service/article.go

寫入內容:傳送門

(2)、打開 service 目錄,新建 cache_service/tag.go

寫入內容:傳送門

這一部分主要是編寫獲取緩存 KEY 的方法,直接參考傳送門便可

4、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) (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(鏈接池)並配置了它的一些參數:

  • Dial:提供建立和配置應用程序鏈接的一個函數
  • TestOnBorrow:可選的應用程序檢查健康功能
  • MaxIdle:最大空閒鏈接數
  • MaxActive:在給定時間內,容許分配的最大鏈接數(當爲零時,沒有限制)
  • IdleTimeout:在給定時間內將會保持空閒狀態,若到達時間限制則關閉鏈接(當爲零時,沒有限制)

二、封裝基礎方法

文件內包含 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 就能夠愉快的調用啦。另外受篇幅限制,這塊的深刻講解會另外開設!

拆解、分層

在先前規劃中,引出幾個方法去優化咱們的應用結構

  • 錯誤提早返回
  • 統一返回方法
  • 抽離 Service,減輕 routers/api 的邏輯,進行分層
  • 增長 gorm 錯誤判斷,讓錯誤提示更明確(增長內部錯誤碼)

編寫返回方法

要讓錯誤提早返回,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 看看我是怎麼寫的,你是怎麼寫的,又分別有什麼優點、劣勢,取長補短一波?

參考

本系列示例代碼

本系列目錄

推薦閱讀

相關文章
相關標籤/搜索