Golang Gin Practice Serials 13 Optimize Your Application Structure and Implement Redis Cache

  golang, javascript, redis

Optimize your application architecture and implement Redis caching

Project address:https://github.com/EDDYCJY/go …

If it is helpful to you, welcome to Star.

Preface

I was thinking before that many tutorials or sample code designs are all in place in one step (no problem)

But can actual readers really understand why? On reflection, with the content of today’s chapter, I think the actual experience will be more impressive.

Planning

In this chapter, the sorting of the following functions will be introduced:

  • Pull-away, hierarchical business logic: logic to mitigate api methods in routers/*.go (but this logic is not important for the time being since this paper does not lay down a hierarchical repository)
  • Increase Fault Tolerance: Judge gorm’s Errors
  • Redis cache: add cache settings to the interface that acquires the data class
  • Reduce duplicate redundant codes

What is the problem?

We found a problem in the planning phase, which is the current pseudo code:

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 the functional logic in the plan is added, the pseudo code will become:

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),
})

If the cache logic is also added, then the iteration will become the same as the following figure.

image

Now that we have found the problem, we should solve this code structure problem in time. At the same time, writing the code clearly, beautifully, legibly and easily is also an important indicator.

How to change?

In the article by the left ear mouse, this kind of code is called “arrow type” code, which has the following problems:

1. My monitor is not wide enough and the arrow code indents too hard. It requires me to pull the horizontal scroll bar back and forth, which makes me quite uncomfortable when reading the code.

2. besides width, there is also length. if-else in if-else, if-else in if-else, there are too many codes in if-else. when you read the middle, you don’t know what kind of layer-by-layer check the middle code went through before you came here.

To sum up, if the “arrow code” is nested too much and the code is too long, it will be quite easy for people (including themselves) who maintain the code to get lost in the code, because when you see the code in the innermost layer, you already don’t know what the previous layer-by-layer condition judgment is and how the code runs here, so the arrow code is very difficult to maintain and Debug.

In short, it isLet the wrong code return first, judge all the wrong judgments before, and then the rest is the normal code.

(Note: This paragraph is quoted from Brother MouseHow to Reconstruct Arrow Code, it is recommended to taste carefully)

workable

This project will optimize the existing code and implement caching. I hope you can learn the method and optimize other places as well.

Step 1: Complete the infrastructure of Redis (you need to install Redis first)

Step 2: Disassemble and stratify the existing codes (the codes of specific steps will not be pasted, I hope you can do some practical operations and deepen your understanding)

Redis

I. configuration

Open conf/app.ini file and add configuration:

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

Second, cache Prefix

Open pkg/e directory, create new cache.go, write content:

package e

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

Third, cache Key

(1) open the service directory and create a new cache_service/article.go

Write content:Portal

(2) open the service directory and create a new cache_service/tag.go

Write content:Portal

This part is mainly to write a method for obtaining cache KEY, which can be directly referred to the transfer gate.

IV. Redis Toolkit

Open pkg directory, create gredis/redis.go, write content:

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
}

Here we have made some basic functional packages.

1. Set RedisConn as redis.Pool and configure some of its parameters:

  • Dial: Provides a function to create and configure application connections
  • TestOnBorrow: Optional Application Check Health Function
  • MaxIdle: maximum number of idle connections
  • MaxActive: The maximum number of connections allowed to be allocated in a given time (when zero, there is no limit)
  • IdleTimeout: will remain idle for a given time and close the connection if the time limit is reached (when it is zero, there is no limit)

2. Packaging Basic Methods

The file contains Set, Exists, Get, Delete, LikeDeletes to support the current business logic, and it involves methods such as:

(1)RedisConn.Get(): Gets an active connection in the connection pool

(2)conn.Do(commandName string, args ...interface{}): Sends a command to the Redis server and returns the reply received

(3)redis.Bool(reply interface{}, err error): Converts the command return to a Boolean value

(4)redis.Bytes(reply interface{}, err error): convert command return to Bytes

(5)redis.Strings(reply interface{}, err error): Converts command return to []string

InredigoIt contains a large number of similar methods, which cannot be changed from its original style. It is recommended to be familiar with its use rules and regulations.Redis commandJust do it

So far, Redis can call happily. In addition, due to space constraints, this in-depth explanation will be offered separately!

Dismantling and layering

In the previous planning, several methods were introduced to optimize our application structure.

  • Error returns early
  • Unified return method
  • Pull out the Service, reduce the logic of routers/api, and carry out layering.
  • Add gorm error judgment to make error prompt clearer (add internal error code)

Write a return method

The invasion of c.JSON is inevitable for errors to return ahead of time, but it can make it more variable. When will it change to XML?

1. Open the pkg directory, create a new app/request.go, and write the file contents:

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. Open the pkg directory, create a new app/response.go, and write the file contents:

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
}

In this way, if you want to change later, you can directly change the method in the app package.

Modifying Existing Logic

Open routers/api/v1/article.go and check the code after modifying the GetArticle method as follows:

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)
}

There are several points worth changing here, mainly due to the addition of error return inside, and direct return if there are errors. In addition, layering is carried out, business logic is cohered into the service layer, while routers/api(controller) is significantly reduced, and the code will be more intuitive.

For example, under service/article_servicearticleService.Get()Methods:

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
}

For gorm’s error return settings, only the models/article.go needs to be modified as follows:

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
}

The habitual increase of. Error will control most of the errors. In addition, it should be noted that in gorm, failure to find records is also considered an “error.”

Last

Obviously, this chapter is not the series you followed me to knock. The topic I gave you is “Implementing Redis Cache and Optimizing Existing Business Logic Code.”

Let it continuously adapt to the development of the business and make the code clearer, easier to read, hierarchical and structural.

If you have doubts, you can comego-gin-exampleLet’s see how I write it, how you write it, and what are the advantages and disadvantages respectively, and learn from each other?

References

This series of sample codes

This series of catalogues

Recommended reading