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 the/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,
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 ๐๐