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 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
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("/", 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.
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.
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()
newCache
invokes the cache.New()
function, which creates a cache with a default expiration time of five minutes and purges expired items every 10 minutesread
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()
functionupdate
sets the value of the key id
to product
, with the default expiration timeAfter 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)) ... }
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
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]