Caching in Golang 🛢️
Tags: go, caching, gofiber, postman
Published: 2023-05-02 12:32:52

##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 :

- [Golang](https://go.dev/doc/install)
- [Fiber](https://docs.gofiber.io/): We'll see this ahead in the tutorial.

##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/](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:

- 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.
- 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,

![Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q7t115p9kefddk89ltw1.png)

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](https://www.postman.com/).

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 ✨💯


![Conclusion](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bne53taz2lbnae0uaw6x.png)



You can find the complete code repository for this tutorial here 👉[Github](https://github.com/Siddheshk02/Go-Cache-API).

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](https://twitter.com/siddhesh1102) and [GitHub](https://github.com/Siddheshk02).

Until then **Keep Learning**, **Keep Building** 🚀🚀