Example of how Golang generics minimize the amount of code you need to write
9 Jun 2022

Example of how Golang generics minimize the amount of code you need to write

I guess that almost everyone in the go community was exciting when Go 1.18 was released, especially because of generics. Some days ago I decided to try generics in the real-world application, by refactoring some of its pieces, related to a caching logic.

In our web application, we have multiple resolvers that execute some sql queries and return data in different types. Obviously, we want to prevent the database overloading by caching the same responses.


    // MyExampleType1 type to serve example response 1.
    type MyExampleType1 struct {}
    // MyExampleType2 type to serve example response 2.
    type MyExampleType2 struct {}

    // MyCachedResolver1 checks if there are any cached data by specific key.
    // If nothing in cache, queries the database and adds result to the redis cache.
    func MyCachedResolver1(redisClient *redis.Client) ([]MyExampleType1, error) {
      key := "resolver_result"
	  value, found := redisClient.Get(ctx, key)
	  if !found {
		value, err := FetchSomethingHeavyFromDB()
		if err != nil {
			return nil, err
		}

		valueJson, err := json.Marshal(value)
		if err != nil {
			return nil, err
		}
		redisClient.Set(context.Background(), key, valueJson, time.Hour*12)
		return value, nil
	  }

	  var val []MyExampleType1
	  err := json.Unmarshal([]byte(value.(string)), &val)
	  if err != nil {
		return nil, err
	  }
	  return val, nil
    }

    // MyCachedResolver2 checks if there are any cached data by specific key.
    // If nothing in cache, queries the database and adds result to the redis cache.
    func MyCachedResolver2(redisClient *redis.Client) ([]MyExampleType2, error) {
      key := "resolver_result_2"
	  value, found := redisClient.Get(ctx, key)
	  if !found {
		value, err := FetchSomethingEvenMoreHeavierFromDB()
		if err != nil {
			return nil, err
		}

		valueJson, err := json.Marshal(value)
		if err != nil {
			return nil, err
		}
		redisClient.Set(context.Background(), key, valueJson, time.Hour*12)
		return value, nil
	  }

	  var val []MyExampleType2
	  err := json.Unmarshal([]byte(value.(string)), &val)
	  if err != nil {
		return nil, err
	  }
	  return val, nil
    }

As you can see, there is some repetitive code that should be written due to the different types MyExampleType1 and MyExampleType2.

Now, let’s see how we can improve this situation by using generics. I’m going to write a function WithCache() which will be responsible for setting and getting data to/from redis cache.

    // WithCache adds exec function result into the redis cache.
    // Pay attention to "T any" that allows us to pass any type to this function.
    func WithCache[T any](redisClient *redis.Client, ctx context.Context, key string, exec func() (T, error), ttl time.Duration) (T, error) {
    	value, found := redisClient.Get(ctx, key)
    	var result T
    	if !found {
            // exec() is a function that should be executed to fetch needed data.
    		value, err := exec()
    		if err != nil {
    			return result, err
    		}
    		jsonValue, err := json.Marshal(value)
    		if err != nil {
    			return result, err
    		}
    		redisClient.Set(ctx, key, jsonValue, ttl)
    		return value, nil
    	}
    	var val T
    	err := json.Unmarshal([]byte(value.(string)), &val)
    	if err != nil {
    		return result, err
    	}
    	return val, nil
    }

After that our resolvers could be refactored into this:

     func MyCachedResolver1(redisClient *redis.Client) ([]MyExampleType1, error) {
       key := "resolver_result_1"
       return WithCache[[]MyExampleType1](redisClient, context.Background(), key, func() ([]MyExampleType1, error) {
		 return FetchSomethingHeavyFromDB()
	   }, time.Hour*12)
     }

     func MyCachedResolver2(redisClient *redis.Client) ([]MyExampleType2, error) {
       key := "resolver_result_2"
       return WithCache[[]MyExampleType2](redisClient, context.Background(), key, func() ([]MyExampleType2, error) {
		 return FetchSomethingEvenMoreHeavierFromDB()
	   }, time.Hour*12)
     }

Now it looks much better and clearer!

Related posts

Concurrent Map Writing and Reading in Go, or how to deal with the data races.
16 Jul 2021

Concurrent Map Writing and Reading in Go, or how to deal with the data races.

This time, I will show you how to work with the maps in go effectively and prevent the occurrence of the data race errors. Data races happen when several goroutines access the same resource concurrently and at least one of the accesses is a write.

go concurrent map data race
Ristretto - the Most Performant Concurrent Cache Library for Go
2 Mar 2021

Ristretto - the Most Performant Concurrent Cache Library for Go

Recently, I discovered a surprisingly reliable memory caching solution, which I’m planning to use in all my further applications to increase performance. In this blog post, I will share some code examples of how you can integrate Ristretto caching library into your application.

go caching ristretto performance
How to Show Flash Messages in Go web applications (with Echo framework)
4 Feb 2021

How to Show Flash Messages in Go web applications (with Echo framework)

When we create a web application, usually, there a need to communicate with the users to inform them about the results of their actions. The easiest way to communicate - is to send messages. These messages might be warnings, errors, or just informational text. In this article, we will improve the UX of our user authentication application from the previous article by adding an error flash message when the user entered a wrong password and a success message after user authorisation.

go echo flash messages
User Authentication in Go Echo with JWT
28 Jan 2021

User Authentication in Go Echo with JWT

In this article, we will build a simple user authentication functionality using JWT (JSON Web Token). In the examples, I’m going to use a Go Echo framework. This will allow us to avoid writing some boilerplate code.

go authentication JWT Echo
A Simple Queue Implementation in Golang with channels
12 Jan 2021

A Simple Queue Implementation in Golang with channels

In this post, I’m going to show the way how we can implement a simple queue in Golang, using channels.

go queue channel goroutine