原文地址:優化你的應用結構和實現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 看看我是怎麼寫的,你是怎麼寫的,又分別有什麼優點、劣勢,取長補短一波?