How to implement in-memory caching in Go

A cache is a hardware or software component that saves data so that subsequent requests for that data can be processed more quickly. Caching, at its most basic, refers to storing and retrieving data from a cache. It is an important concept that enables us to significantly increase the performance of an application.

Sometimes, an application may start to slow down due to the number of users, requests, or services. Caching offers a solution that might come in handy. There are several ways to implement in-memory caching in Go. This article will discuss how to implement in-memory caching using the go-cache package.

To understand in-memory caching using go-cache, we will build a simple web server using the HttpRouter package. This web server will demonstrate how and when to use the caching mechanism to increase the performance of our application.

Jump ahead:

go-cache

go-cache is an in-memory key:value store/cache that is similar to memcached and works well with applications that run on a single machine.

To install the go-cache and HttpRouter packages, run the following commands in your terminal:

go get github.com/patrickmn/go-cache
go get github.com/julienschmidt/httprouter

Creating a simple web server in Go

Run the following commands to create a directory called caching:

mkdir caching
cd caching

Next, we’ll enable dependency tracking with this command:

go mod init example/go_cache

Then, we’ll create a main.go file:

touch main.go

In main.go, the code will look like this:

package main
import (
    "fmt"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!n")
}

func main() {
    router := httprouter.New()
    router.GET("https://247webdevs.blogspot.com/", Index)

    err := http.ListenAndServe(":8080", router)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Server running on :8080")
}

Finally, to start the server, run:

go run .

The server is running in Port 8080.

Adding routes to our server

We have built a simple web server in Go, but it does nothing. Let’s make our server connect to an external API to query some data. This demonstrates how a web application often works. More often than not, a web app performs some network operations, highly computational tasks, and database queries.

https://fakestoreapi.com/ API provides us with a mock API. We’ll have to update the content of main.go to contain the following lines of code:

import (
  ...
  "encoding/json"
  "io"
)

type Product struct {
    Price       float64 `json:"price"`
    ID          int     `json:"id"`
    Title       string  `json:"title"`
    Category    string  `json:"category"`
    Description string  `json:"description`
    Image       string  `json:"image"`
}

func getProduct(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    id := p.ByName("id")
    resp, err := http.Get("https://fakestoreapi.com/products/" + id)
    if err != nil {
        log.Fatal(err)
        return
    }
    defer resp.Body.Close()
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
        return
    }
    product := Product{}
    parseErr := json.Unmarshal(body, &product)
    if parseErr != nil {
        log.Fatal(parseErr)
        return
    }
    response, ok := json.Marshal(product)
    if ok != nil {
        log.Fatal("somethng went wrong")
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(response)
}

func main() {
    ... 
    router.GET("/product/:id", getProduct)
    ...
}

Using the HttpRouter package, we create a product/:id endpoint that accepts a GET request. The router uses the getProduct function to handle the incoming requests to the endpoint.

Using go-cache for caching

Each network request will take a couple of milliseconds depending on how fast the user’s network connection is. Some requests might require high CPU usage. It is better to store the result of such requests in memory for quick retrieval, barring any updates to the underlying data, which might cause changes to the returned data.

The following code goes in the main.go file. We start by importing the go-cache package alongside the time package:

import (
    ...
    "time"
    "github.com/patrickmn/go-cache"
)

The code below helps to initialize the cache along with read and update methods that allow us to retrieve and input data to and from the cache:

type allCache struct {
    products *cache.Cache
}

const (
    defaultExpiration = 5 * time.Minute
    purgeTime         = 10 * time.Minute
)

func newCache() *allCache {
    Cache := cache.New(defaultExpiration, purgeTime)
    return &allCache{
        products: Cache,
    }
}

func (c *allCache) read(id string) (item []byte, ok bool) {
    product, ok := c.products.Get(id)
    if ok {
        log.Println("from cache")
        res, err := json.Marshal(product.(Product))
        if err != nil {
            log.Fatal("Error")
        }
        return res, true
    }
    return nil, false
}

func (c *allCache) update(id string, product Product) {
    c.products.Set(id, product, cache.DefaultExpiration)
}

var c = newCache()
  1. newCache invokes the cache.New() function, which creates a cache with a default expiration time of five minutes and purges expired items every 10 minutes
  2. read invokes the cache.Get(key) function, which retrieves an item with the given key. Type assertion is carried out on the retrieved item, so it can be passed to functions that don’t accept arbitrary types. The result is parsed to JSON format using the JSON.Marshal() function
  3. update sets the value of the key id to product, with the default expiration time

After we’re done with the initialization of our cache, we have to think about how we want to implement the cache.

Usually, the approach is to check the cache for the requested resource. If it’s found in the cache, it’s returned to the client. However, if it’s not found, we proceed as usual to perform whatever action is needed to get the desired resource. Then the result is stored in the cache.

A programming concept that can help us perform the described action is middleware. A basic middleware in Go usually has this form:

func something(f http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("Request recieved")
        f(w, r)
    }
}

Following that pattern, we can create a checkCache middleware with HttpRouter:

func checkCache(f httprouter.Handle) httprouter.Handle {
    return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
        id := p.ByName("id")
        res, ok := c.read(id)
        if ok {
            w.Header().Set("Content-Type", "application/json")
            w.Write(res)
            return
        }
        log.Println("From Controller")
        f(w, r, p)
    }
}

The middleware here takes a httprouter.Handle as one of its parameters, wraps it, and returns a new httprouter.Handle for the server to call.

We call the c.read method with the id as its argument. If a product is found, we return that without proceeding any further.

We call the c.update method to save the retrieved product to the cache:

func getProduct(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    ...
    c.update(id, product)
    w.Header().Set("Content-Type", "application/json")
    w.Write(response)
}

Finally, we pass the getProduct function as an argument to the checkCache, therefore enabling the middleware on the endpoint. Requests to this endpoint will now make use of our cache:

func main() {
    ...
    router.GET("/product/:id", checkCache(getProduct))
    ...
}

Benchmark testing

We’ve said that caching significantly improves the performance of our application. To support that claim, let’s perform some benchmarks.

The benchmarking tool of choice here is go-wrk, which you can install with this command:

go install github.com/tsliwowicz/go-wrk@latest

Next, we need to test our application with and without the caching middleware:

func main() {
    ...
    router.GET("/product/:id", checkCache(getProduct))
    ...
}

With the caching middleware active, run:

 go-wrk -c 80 -d 5  http://127.0.0.1:8080/product/1

Update the route to disable caching as follows:

func main() {
    ...
    router.GET("/product/:id", getProduct)
    ...
}

Restart the server, then run the command below:

 go-wrk -c 80 -d 5  http://127.0.0.1:8080/product/1

Benchmark Without Caching

With 80 connections running for five seconds, we get the above results. The cached route could handle significantly more requests than the route that was not cached.

Conclusion

In this article, we discussed how to implement in-memory caching using the go-cache package. go-cache is just one of many packages available to handle in-memory caching in Go. Keep in mind that no matter the tool you choose to use, the underlying principle remains the same, and the benefits of caching are undoubtedly clear throughout the software development industry. It would be in your best interest to adopt the practice of caching in your next app or existing codebase.

The post How to implement in-memory caching in Go appeared first on LogRocket Blog.

from LogRocket Blog https://ift.tt/HFeTfaB
Gain $200 in a week
via Read more


Source link