Caching in Golang

In-Memory Caching in Golang using Fiber Framework

ยท

9 min read

Caching in Golang

Introduction

Caching is an important technique used in web development to improve the performance of applications. It involves storing frequently accessed data in a temporary storage area, such as in-memory, to reduce the amount of time required to fetch the data from its source.

There are different types of caching, including in-memory caching, database caching, and file-based caching. Each type has its advantages and disadvantages, and choosing the right one depends on the specific use case and the requirements of the application.

In-memory caching is one of the simplest and fastest caching methods available, making it a popular choice for many applications. It involves storing data in the memory of the server, making it quickly accessible to the application without the need for any external dependencies.

In this blog post, we will focus on in-memory caching in Golang and demonstrate its implementation using a simple API as an example. By the end of this blog, you'll have a solid understanding of in-memory caching works.

Prerequisites

To continue with the tutorial, firstly you need to have Golang and Fiber installed.

Installations :

Getting Started ๐Ÿš€

Let's get started by creating the main project directory Go-Cache-API by using the following command.

(๐ŸŸฅBe careful, sometimes I've done the explanation by commenting in the code)

mkdir Go-Cache-API //Creates a 'Go-Cache-API' directory
cd Go-Cache-API //Change directory to 'Go-Cache-API'

Now initialize a mod file. (If you publish a module, this must be a path from which your module can be downloaded by Go tools. That would be your code's repository.)

go mod init github.com/<username>/Go-Cache-API //<username> is your github username

To install the Fiber Framework run the following command :

go get -u github.com/gofiber/fiber/v2

For implementing the In-Memory Caching we are going to use go-cache , to install it run the following command :

go get github.com/patrickmn/go-cache

go-cache is an in-memory key: value store/cache similar to memcached that is suitable for applications running on a single machine.

Now, let's make the main.go in which we are going to define the routes.

package main

import (
    "time"
    "github.com/gofiber/fiber/v2"
    "github.com/Siddheshk02/Go-Cache-API/middleware"
    "github.com/Siddheshk02/Go-Cache-API/routes"
    "github.com/patrickmn/go-cache"
)

func main() {
    app := fiber.New() // Creating a new instance of Fiber.

    //cache := cache.New(10*time.Minute, 20*time.Minute) // setting default expiration time and clearance time.

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("Hello, World ๐Ÿ‘‹!")
    })
    //app.Get("/posts/:id", middleware.CacheMiddleware(cache),   routes.GetPosts) //commenting this route just to test the "/" endpoint.
    app.Listen(":8080")
}

In the main.go file, the first step is to initialize a new Fiber app using the fiber.New() method. This creates a new instance of the Fiber framework that will handle the HTTP requests and responses.

We are going to use https://jsonplaceholder.typicode.com/ for fetching example data. We are going to fetch data from /posts endpoint. Jsonplaceholder provides fake data for many other api endpoints you can try for any.

The cache.New() function takes two arguments:

  1. The first argument is the amount of time that a cache entry should live before it's automatically evicted. In this case, it's set to 10 minutes.

  2. The second argument is the amount of time that a cache entry can remain idle (without being accessed) before it's automatically evicted. In this case, it's set to 20 minutes.

After running the go run main.go command the terminal will look like this,

You can now uncomment all the code lines.

As you can see a middleware function is added to the /posts route. When a request is made to this endpoint, the server will first execute the CacheMiddleware function with a cache parameter. This middleware function is responsible for checking if the requested data is already cached and returning it from the cache instead of making a new API call. If the data is not present in the cache, the middleware function will pass the request to the next function in the middleware chain, which is routes.GetPosts.

routes.GetPosts is a function that will be executed when the request reaches this point. This function will handle the request by making a GET request to the external API at https://jsonplaceholder.typicode.com/posts/:id to fetch the post data. The :id part of the URL is a placeholder that will be replaced with the actual ID of the post being requested.

Let's define the CacheMiddleware() function for this, make a folder middleware in the main directory. In this make a file cache.go.

package middleware

import (
    "encoding/json"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/patrickmn/go-cache"
)

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

func CacheMiddleware(cache *cache.Cache) fiber.Handler {
    return func(c *fiber.Ctx) error {
        if c.Method() != "GET" {
            // Only cache GET requests
            return c.Next()
        }

        cacheKey := c.Path() + "?" + c.Params("id") // Generate a cache key from the request path and query parameters

        // Check if the response is already in the cache
        if cached, found := cache.Get(cacheKey); found {
            return c.JSON(cached)
        }
        err := c.Next()
        if err != nil {
            return err
        }

        var data Post
        cacheKey := c.Path() + "?" + c.Params("id")

        body := c.Response().Body()
        err = json.Unmarshal(body, &data)
        if err != nil {
            return c.JSON(fiber.Map{"error": err.Error()})
        }

        // Cache the response for 10 minutes
        cache.Set(cacheKey, data, 10*time.Minute)

        return nil
    }
}

The middleware function CacheMiddleware that takes a pointer to a cache.Cache object as an argument and return a fiber.Handler function.

The middleware function first checks if the HTTP method of the request is GET. If it is not a GET request, it simply passes the request to the next middleware function.

If it is a GET request, it generates a cache key by concatenating the request path and query parameters. It then checks if the response for that cache key is already present in the cache by using the Get method of the cache.Cache object. If the response is present in the cache, it returns the cached response using the JSON method of the fiber.Ctx object.

If the response is not present in the cache, it calls the next middleware function by using the Next method of the fiber.Ctx object. It then creates a new cache key by concatenating the request path and query parameters, and reads the response body using the Response().Body() method of the fiber.Ctx object. It then unmarshals the response body into a Post struct using the json.Unmarshal method.

If there is an error in unmarshaling the response body, it returns an error response using the JSON method of the fiber.Ctx object.

If there is no error, it caches the response for 10 minutes using the Set method of the cache.Cache object and returns nil to indicate that the middleware function has completed processing the request.

Now, let's define the GetPosts() function for this, make a folder routes in the main directory. In this make a file routes.go.

package routes

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"

    "github.com/gofiber/fiber/v2"
)

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

func GetPosts(c *fiber.Ctx) error {
    id := c.Params("id") // Get the post ID from the request URL parameters
    if id == "" {
        log.Fatal("Invalid ID")
    }

    // Fetch the post data from the API
    resp, err := http.Get("https://jsonplaceholder.typicode.com/posts/" + id)
    if err != nil {
        return c.JSON(fiber.Map{"error": err.Error()})
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return c.JSON(fiber.Map{"error": err.Error()})
    }

    var data Post
    err = json.Unmarshal(body, &data)
    if err != nil {
        return c.JSON(fiber.Map{"error": err.Error()})
    }

    return c.JSON(data)

}

This function first extracts the ID parameter from the URL using c.Params("id"). If the ID is not provided or is invalid, it logs a fatal error.

It then makes an HTTP GET request to the https://jsonplaceholder.typicode.com/posts/ API with the provided ID as the endpoint. It checks for any errors during the request and returns an error response if an error occurs.

The response body is then read using the ioutil.ReadAll() function and unmarshaled into a Post struct using json.Unmarshal(). If an error occurs during the unmarshaling, an error response is returned.

Finally, the function returns a JSON response with the Post data. this data is then stored in the cache by the CacheMiddleware() function.

Now, if we want to determine if the response is coming from the cache or the API server, we'll add a header to the response indicating whether the response was served from the cache or not. We'll add a custom header named Cache-Status and set its value to HIT or MISS depending on whether the response was served from the cache or not. So, the CacheMiddleware() function will look like :

package middleware

import (
    "encoding/json"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/patrickmn/go-cache"
)

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

func CacheMiddleware(cache *cache.Cache) fiber.Handler {
    return func(c *fiber.Ctx) error {
        if c.Method() != "GET" {
            // Only cache GET requests
            return c.Next()
        }

        cacheKey := c.Path() + "?" + c.Params("id") // Generate a cache key from the request path and query parameters

        // Check if the response is already in the cache
        if cached, found := cache.Get(cacheKey); found {
            c.Response().Header.Set("Cache-Status", "HIT")
            return c.JSON(cached)
        }

        c.Set("Cache-Status", "MISS")
        err := c.Next()
        if err != nil {
            return err
        }

        var data Post
        cacheKey := c.Path() + "?" + c.Params("id")

        body := c.Response().Body()
        err = json.Unmarshal(body, &data)
        if err != nil {
            return c.JSON(fiber.Map{"error": err.Error()})
        }

        // Cache the response for 10 minutes
        cache.Set(cacheKey, data, 10*time.Minute)

        return nil
    }
}

Let's test the API, run go run main.go in the terminal, after getting the same output as earlier, open any API testing tool for example Postman.

Enter the URL : http://127.0.0.1:8080/posts/1 then press enter.

Output :

{
    "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"
}

Now click on the Headers option, you can see Cache-Status : MISS. Press Enter again for the same ID and see the Cache-Status. Now it is changed to HIT. This shows that the second time when the same parameters were passed the response was sent through the cache.

This is the Basic implementation of In-Memory Caching in Golang.

Conclusion โœจ๐Ÿ’ฏ

You can find the complete code repository for this tutorial here ๐Ÿ‘‰Github.

In Golang, you can implement caching using in-memory cache systems like the one we discussed in this blog post. By using a middleware function, you can easily integrate caching into your Golang web applications and reduce the response time for your users.

However, caching can also lead to stale data if not managed properly. It is important to consider the cache expiration time and update the cache whenever the underlying data changes.

Overall, caching can be a great addition to your web development toolkit, and I hope this blog post has provided a useful introduction to in-memory caching in Golang.

To get more information about Golang concepts, projects, etc. and to stay updated on the Tutorials do follow Siddhesh on Twitter and GitHub.

Until then Keep Learning, Keep Building ๐Ÿš€๐Ÿš€

Did you find this article valuable?

Support CloudNativeFolks Community by becoming a sponsor. Any amount is appreciated!

ย