Simple and Easy in-memory cache in Golang
Tags: go, tutorial, beginners, webdev
Published: 2021-08-25 15:38:26

We often end up caching our application data in solutions like memcached or Redis. However, not all applications have to go that way and for the overwhelming majority of cases (especially small or personal projects) they don't need to use an external source.

Exactly for this reason I decided to create an example to show how to cache data in our application with a simple approach.

The framework I'm going to use is [Fiber](https://gofiber.io/), in addition to being very intuitive and easy to use, the main reason for its use is that if you're a JavaScript/TypeScript developer like me, you'll feel at home because Fiber was inspired by [Express](https://expressjs.com/).

And the library that will be used to cache our application data is [ttlcache](https://github.com/ReneKroon/ttlcache), because it is easy to use and has an immensely intuitive API.

The idea of today's application is to make a request to the [JSONPlaceholder](https://jsonplaceholder.typicode.com/) API and we will get a todo according to the id that is provided in the parameters.

# Let's code

First let's install the following packages:

```sh
go get github.com/gofiber/fiber/v2
go get github.com/ReneKroon/ttlcache/v2
```

Then let's create a simple API:

```go
package main

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

func main() {
    app := fiber.New()

    app.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("It seems to be working 🥳")
    })

    app.Listen(":3000")
}
```

Now if you open a new tab in your browser and visit `http://localhost:3000`, in the body of the page you should have the message `It seems to be working 🥳`.

Now with our basic API working we can make an http request to the JSONPlaceholder API via the address `https://jsonplaceholder.typicode.com/todos/1` and the data we got is the following:

```js
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}
```

Now we can start by creating our struct which we'll name **Todo**, which will contain the following properties:

```go
type Todo struct {
    Userid    int    `json:"userId"`
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}
```

This way we can make some changes to our endpoint, first we will add the `id` parameter. Then we will get the value of it using the `c.Params()` function.

```go
app.Get("/:id", func(c *fiber.Ctx) error {
    id := c.Params("id")
    // ...
})
```

Now we will make the http request to fetch only a todo according to the `id` that is passed in the parameters. And we will return that same todo.

```go
app.Get("/:id", func(c *fiber.Ctx) error {
    id := c.Params("id")
    res, err := http.Get("https://jsonplaceholder.typicode.com/todos/" + id)
    if err != nil {
        return err
    }

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

    todo := Todo{}
    parseErr := json.Unmarshal(body, &todo)
    if parseErr != nil {
        return parseErr
    }

    return c.JSON(fiber.Map{"Data": todo})
})
```

Now if we test our api by making an http request at the address `http://localhost:3000/[id]` we will get the data of its todo.

However, we will be constantly getting data from the JSONPlaceholder API, but what we want is to cache the data so that next time the data will come from the cache and not the JSONPlaceholder.

So we'll import the ttlcache library into our project and then we'll create a new instance of ttlcache, which we'll name *cache*.

```go
package main

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

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

type Todo struct {
    Userid    int    `json:"userId"`
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

var cache ttlcache.SimpleCache = ttlcache.NewCache()

// ...
```

Then in our main function, we will specify how long the data will persist in the cache, in this example I defined that it persisted for **10s**.

```go
func main() {
    app := fiber.New()

    cache.SetTTL(time.Duration(10 * time.Second))
    // ...
}
```

Now inside our endpoint, let's cache the data before returning it in the response body. 

For that we will use the `cache.Set()` function which will receive two arguments, the first is the **key** which in this case will be the *id* and the second argument is the **data**, which in this case will be the *todo*.

```go
app.Get("/:id", func(c *fiber.Ctx) error {
    // ...

    cache.Set(id, todo)
    return c.JSON(fiber.Map{"Data": todo})
})
```

Now we will have all cached, but we haven't finished this yet because we still need to create a middleware. What this middleware will do is check if the key already exists in the cache, if it does we will return the data we have stored in the cache. 

But if the key doesn't exist in the cache, we will execute the next method of our route, which in this case will be the execution of the http request.

```go
func verifyCache(c *fiber.Ctx) error {
    // ...
}
```

First we have to get the value of the id parameter. Then we will use the `cache.Get()` function which will receive a single argument, which is the **key**, which in this case is the *id*.

If the key exists, we will return its data, otherwise we will proceed to the next method to perform the http request, using the `c.Next()` function.

```go
func verifyCache(c *fiber.Ctx) error {
    id := c.Params("id")
    val, err := cache.Get(id)
    if err != ttlcache.ErrNotFound {
        return c.JSON(fiber.Map{"Cached": val})
    }
    return c.Next()
}
```

Last but not least we need to add middleware to our route, like this:

```go
app.Get("/:id", verifyCache, func(c *fiber.Ctx) error {
    // ...
})
```

The final code should look like the following:

```go
package main

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

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

type Todo struct {
	Userid    int    `json:"userId"`
	ID        int    `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

var cache ttlcache.SimpleCache = ttlcache.NewCache()

func verifyCache(c *fiber.Ctx) error {
	id := c.Params("id")
	val, err := cache.Get(id)
	if err != ttlcache.ErrNotFound {
		return c.JSON(fiber.Map{"Cached": val})
	}
	return c.Next()
}

func main() {
	app := fiber.New()

	cache.SetTTL(time.Duration(10 * time.Second))

	app.Get("/:id", verifyCache, func(c *fiber.Ctx) error {
		id := c.Params("id")
		res, err := http.Get("https://jsonplaceholder.typicode.com/todos/" + id)
		if err != nil {
			return err
		}

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

		todo := Todo{}
		parseErr := json.Unmarshal(body, &todo)
		if parseErr != nil {
			return parseErr
		}

		cache.Set(id, todo)
		return c.JSON(fiber.Map{"Data": todo})
	})

	app.Listen(":3000")
}
```

Now, if we are going to test the performance of our app, we might notice a big difference in its performance. Without the use of the cache each request took an average of **376ms**, after the implementation of the cache each request took an average of **2ms**.

# Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧐

Hope you have a great day! 😎