Skip to content
Home » Caching with Redis in Golang

Caching with Redis in Golang

What is Redis?

Redis is an open-source data storage system that stores information in the computer’s memory. It is a versatile solution for database, cache, and message broker needs. Its simplicity, high performance, speed, and flexibility make it widely adopted in various applications and systems.

Redis logo

Here are some common applications of Redis:

  • Caching: Redis can be used for caching by storing frequently accessed data.
  • Rate Limiting: Redis can be used as a rate limiter to control the frequency of requests from clients.
  • Queuing System: Redis supports the list data structure that can be utilized as a message queue to manage tasks and distribute workload among multiple workers or processes.
  • Geospatial Indexing: Redis supports geospatial data storage that can be used for location-based recommendations and geofencing.

Pre-requisites

Code for caching data using Redis

package main

import (
        "context"
        "encoding/json"
        "fmt"
        "github.com/gin-gonic/gin"
        "github.com/redis/go-redis/v9"
        "io/ioutil"
        "net/http"
        "time"
)

type Post struct {
        UserID int    `json:"userId"`
        ID     int    `json:"id"`
        Title  string `json:"title"`
        Body   string `json:"body"`
}

func getPosts(c *gin.Context) {
        val, err := redisInstance.Get(ctx, "posts").Bytes()
        if err != nil {
                // Calling the API
                res, err := http.Get("https://jsonplaceholder.typicode.com/posts")

                defer res.Body.Close()
                body, err := ioutil.ReadAll(res.Body)
                if err != nil {
                        return
                }

                // Parsing the JSON
                var parsedJSONObject []Post
                json.Unmarshal(body, &parsedJSONObject)

                // Setting the key
                redisErr := redisInstance.Set(ctx, "posts", body, 20*time.Second).Err()
                if redisErr != nil {
                        panic(redisErr)
                }
                fmt.Println("Cache Miss")
                c.IndentedJSON(http.StatusOK, parsedJSONObject)
        } else {
                posts := []Post{}
                err = json.Unmarshal(val, &posts)
                if err != nil {
                        panic(err)
                }
                fmt.Println("Cache Hit")
                c.IndentedJSON(http.StatusOK, posts)
        }
}

var ctx = context.Background()

var redisInstance = redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
})

func main() {
        router := gin.Default()

        router.GET("/posts", getPosts)

        router.Run(":8000")
}
Go


Let’s go through the code step by step:

Line 1: This line indicates that the code belongs to the package named main.

Line 3-12: Imports multiple packages in a single import statement. The packages being imported are:

  • context: It provides the ability to manage and propagate context across goroutines.
  • encoding/json: It provides functions for encoding and decoding JSON data.
  • fmt: It provides functions to print formatted output and scan input from the standard input/output streams.
  • github.com/gin-gonic/gin: It is a third-party package named gin-gonic/gin. It is a web framework for building HTTP servers and routing requests.
  • github.com/redis/go-redis/v9: It is a third-party package named go-redis. It is a Redis client library for Go that allows the program to interact with Redis databases.
  • io/ioutil: It provides utility functions for input/output operations, such as reading and writing files.
  • net/http: It allows the program to send HTTP requests, handle responses, and build HTTP servers.
  • time: It provides functions to work with dates, times, durations, etc.

Line 14-19: Defines a new struct type named Post. It contains the following fields,

  • UserID: It is of type int and is tagged with json:"userId".
  • ID: It is of type int and is tagged with json:"id".
  • Title: It is of type string and is tagged with json:"title".
  • Body: It is of type string and is tagged with json:"body".
Note: The json tag specifies the mapping between the field name in the Go struct and the corresponding JSON key when encoding or decoding JSON data.


Line 55: Assign context.Background() function to a variable named ctx. The context.Background() returns an empty context, and it is used as a starting point for creating more specific contexts that carry additional information or have specific behaviors.

Line 57-59: Creates a new instance of Redis client using the NewClient function provided by the go-redis package. The NewClient function accepts a redis.Options struct and initializes Addr field so that it tells the Redis client instance to connect to that particular server. In our case, it will be the Redis server running locally on port 6379.

Line 61: Defines the main function and it is the entry point of this program, where the execution starts.

Line 62: Calls the gin.Default() function and assigns it to a variable named router. The gin.Default() creates a new instance of the Gin router with default configurations and middleware.

Line 64: Defines a route for handling HTTP GET request using router.GET() method. The route path /post and the route handler function getPosts is passed in to the router.GET() method. So when a GET request is made to the /posts endpoint, the getPosts function will be executed.

Line 66: Initiates the server to listen on the port 8000.

Now let’s go through the getPosts route handler function step by step.

Line 21: Defines a function getPosts and c is the parameter of the type gin.Context which is provided by the Gin framework that represents the context of an HTTP request and response.

Line 22: Retrieves the value associated with the posts key from Redis database using the redisInstance.Get() method. The redisInstance.Get() method takes two arguments: a context object (ctx) and the key for which the value is being retrieved (posts). The Bytes() method is called on the result of the redisInstance.Get() method to convert the retrieved value into a byte slice ([]byte). The retrieved value is then stored in the val variable and any error that occurs is stored in the err variable.

Line 23: If the err variable is not nil then the below code block will be executed.

Line 25: An HTTP GET request is made to the specified URL using the http.Get() method and returns an HTTP response and an error. The response and error are stored in the variables res and err.

Line 27: Ensures that the response body is closed after the function has finished executing using defer keyword.

Line 28: Reads all the data from the res.Body object using ioutil.ReadAll() function from the Go standard library.

Line 29-31: This code block checks if an error occurred while reading the response body. If an error is present, it means that there was an issue with reading the response body data, and the code exits early, ensuring that the subsequent code block is not executed.

Line 34: Declares a variable named parsedJSONObject of the type []Post, the struct type we defined earlier.

Line 35: Converts the JSON data stored in the body variable and it into a slice of Post struct type by unmarshaling the JSON using the json.Unmarshal() function.

Line 38: Sets a value in Redis database using the Set() method of a Redis client instance redisInstance. The Set() method here accepts four arguments,

  • ctx: It is the context object that provides control over the execution and cancellation of operations.
  • posts“: It is the key under which the value is stored in Redis database.
  • body: It is the value to be stored in the Redis database.
  • 20*time.Second: It is the expiration time for the cached value. In this case, the value will expire and be automatically evicted from the cache after 20 seconds.

The Err() method is called to retrieve the error, if any, that occurred during the execution of the Set() operation.

Line 39-41: If redisErr is not nil, indicating an error occurred, it is handled by panicking, which is a way to abort the program execution and provides an error message.

Line 42: Prints the string “Cache Miss” to the standard output using the Println() function of the fmt package.

Line 43: Sends a JSON response to the client with the serialized JSON data representing the parsedJSONObject variable using the IndentedJSON() method of the Gin context c. The response will have the HTTP status code 200 (OK).

Line 44: If the err variable is nil then the below code block will be executed.

Line 45: Declares and initializes a variable named posts as an empty slice of Post structs.

Line 46: Unmarshals the JSON data stored in the val variable into a slice of Post structs referred to by the &posts pointer using json.Unmarshal() function. If there are any errors during the unmarshaling process, the err variable will contain the corresponding error message.

Line 47-49: If err exists, indicating an error occurred, it is handled by panicking.

Line 50: Prints the string “Cache Hit” to the standard output using the Println() function of the fmt package.

Line 51: Sends a JSON response to the client with the serialized JSON data representing the posts variable using the IndentedJSON() method of the Gin context c. The response will have the HTTP status code 200 (OK).

Output

To start the server, enter the following command in the terminal.

$ go run main.go

> [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /posts                    --> main.getPosts (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8000


Let’s test the output of multiple scenarios:

Scenario 1: The list of posts is not cached in the Redis database.
Enter the following command in the terminal to make a cURL request to the server.

$ curl localhost:8000/posts

> [
    {
        "userId": 1,
        "id": 1,
        "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
        "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
    },
    {
        "userId": 1,
        "id": 2,
        "title": "qui est esse",
        "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
    },
    .
    .
    .
    .
    .
  ]


We observed a “Cache Miss” message in the server logs, and it took approximately 178ms to process the request.

Cache Miss
[GIN] 2023/05/30 - 17:18:11 | 200 |  177.675583ms |             ::1 | GET      "/posts"


Scenario 2: The list of posts is cached in the Redis database.

We observed a “Cache Hit” message in the server logs, and it took approximately 4ms to process the request.

Cache Hit
[GIN] 2023/05/30 - 17:18:12 | 200 |    3.147042ms |             ::1 | GET      "/posts"


Scenario 3: The list of posts in the cached gets expired after 20 seconds from the Redis database.

We observed a “Cache Miss” message in the server logs, and it took approximately 55ms to process the request.

Cache Miss
[GIN] 2023/05/30 - 17:18:41 | 200 |   54.169083ms |             ::1 | GET      "/posts"

Source Code

Github: https://github.com/cod3kid/blog/tree/main/redis-caching-in-golang