Building a Generic Cache With Go 1.18

Posted on May 21, 2022

At GUTS we have a “hack day” each month. During this day the devteam deviates from the regular story board and people work individually or in teams on interesting, somewhat related projects and experiments or experiment with new frameworks or languages.

My recent project was to build a generics based cache to replace all the custom, ad-hoc memory caches we currently have with all the quirky type assertions.

The implementation is pretty straightforward - generics work and behave as you expect and the end result is a nice usable generic cache

cache := New[string, string]()
cache.Set("hello", "world")
fmt.Println(cache.Get("hello"))

It’s not optimized for huge datasets, doesn’t have enough test coverage yet but we might use this, at some point, in our production code.

Stuff that’s lacking

  • github actions. There are tests, but they’re not run on each commit/PR
  • tests. Again, there are tests, but not enough
  • releases. Didn’t create any yet, that simple
  • Configuration. There’s a lot to configure on a cache but the API quickly gets convulted. More on this in the next section

Configuration

Go does not support optional arguments, named arguments or argument defaults. This means it’s tricky to define a default configuration and allow the caller to only override the few values they want to change - they have to specify all options, and since the arguments are unnamed, you end up with large, incomprehensible sets of arguments. E.g.

f := NewFoo("bar", true, 42, []string{"blah"}, i)

This makes it really hard to see, at a glance, which options means what.

There are some (anti) patterns to work around this such as

variadic/optional arguments with reflection:

func NewFoo(options ...any)

But this still doesn’t give you named arguments, only works with a few arguments (since the specific set of arguments determins what means what) and you lose lots of compiler checks

the above but then in a map, so that you can name the values

func NewFoo(Options{"name":"bar", debug: true})

This does not make the options optional unless you define them as variadic as well ( ...Options), but then what if you pass multiple Options{}? It’s still a confusing pattern and allows many ways to shoot yourself in the foot.

A configuration struct

You can define all your configuration in a struct that can be passed to your New(). You can chose to use the variadic argument trick to make it optional but, again what if you pass multiple?

Also, all unspecified properties will default to their zero value which may not be the value you actually want, so some properties are still not optinal. E.g. a bool will always be false

Functional options

Dave Cheney’s post/presentation “Functional options for friendly APIs” illustrates some of the above cases and provides an alternative, functional options.

Here you have variadic options, but they’re callables that get invoked with the struct and set the values on it. The end results seems like a very programmer friendly and robust configuration mechanism.

This sounds like a nice trick to apply to the cache implementation. To be continued I guess…