最佳化你的應用結構和實現Redis緩衝
項目地址:https://github.com/EDDYCJY/go...
如果對你有所協助,歡迎點個 Star
前言
之前就在想,不少教程或樣本的代碼設計都是一步到位的(也沒問題)
但實際操作的讀者真的能夠理解透徹為什麼嗎?左思右想,有了今天這一章的內容,我認為實際經曆過一遍印象會更加深刻
規劃
在本章節,將介紹以下功能的整理:
- 抽離、分層商務邏輯:減輕 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:6379Password =MaxIdle = 30MaxActive = 30IdleTimeout = 200
二、緩衝 Prefix
開啟 pkg/e 目錄,建立 cache.go,寫入內容:
package econst ( 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 gredisimport ( "encoding/json" "time" "github.com/gomodule/redigo/redis" "github.com/EDDYCJY/go-gin-example/pkg/setting")var RedisConn *redis.Poolfunc 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}
在這裡我們做了一些基礎功能封裝
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
在 redigo 中包含大量類似的方法,萬變不離其宗,建議熟悉其使用規則和 Redis命令 即可
到這裡為止,Redis 就可以愉快的調用啦。另外受篇幅限制,這塊的深入講解會另外開設!
拆解、分層
在先前規劃中,引出幾個方法去最佳化我們的應用結構
- 錯誤提前返回
- 統一返回方法
- 抽離 Service,減輕 routers/api 的邏輯,進行分層
- 增加 gorm 錯誤判斷,讓錯誤提示更明確(增加內部錯誤碼)
編寫返回方法
要讓錯誤提前返回,c.JSON 的侵入是不可避免的,但是可以讓其更具可變性,指不定哪天就變 XML 了呢?
1、開啟 pkg 目錄,建立 app/request.go,寫入檔案內容:
package appimport ( "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 appimport ( "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 看看我是怎麼寫的,你是怎麼寫的,又分別有什麼優勢、劣勢,取長補短一波?
參考
本系列範例程式碼
本系列目錄
- Gin實踐 連載一 Golang介紹與環境安裝
- Gin實踐 連載二 搭建Blog API's(一)
- Gin實踐 連載三 搭建Blog API's(二)
- Gin實踐 連載四 搭建Blog API's(三)
- Gin實踐 連載五 搭建Blog API's(四)
- Gin實踐 連載六 搭建Blog API's(五)
- Gin實踐 連載七 Golang優雅重啟HTTP服務
- Gin實踐 連載八 為它加上Swagger
- Gin實踐 連載九 將Golang應用部署到Docker
- Gin實踐 連載十 定製 GORM Callbacks
- Gin實踐 連載十一 Cron定時任務
- Gin實踐 連載十二 最佳化配置結構及實現圖片上傳
- Gin實踐 番外 Golang交叉編譯
推薦閱讀