Go adventures: Optional values
One of my biggest pet peeves in Go was the lack of vocabulary to express optional values. In episode 2 of Go adventures, we dive into the old and new solutions.
The old days
Since Go aims to be a simple language, a lot of features are missing — even some that a lot of people consider to be essential (enums anyone?). Prior to Go gaining generics support in version 1.18, the landscape of tools to represent optional values was bleak. You could either try to bend the built-in features of the language, or depend on libs that were wrapping each type individually, with no or inconvenient support for non-primitive types, such as markphelps/optional.
Let’s take a look at the language built-in features:
1. Pointers
This is probably the most used solution based on my experience. It works, but there are several problems with it. The biggest concern with this method is safety. Nothing stops people from trying to dereference a nil pointer, causing the program to panic.
Then there is also the fact that it’s very implicit. You can never really be sure why it’s a pointer. Is it a big object that somebody didn’t want to copy? Or is it an optional? Maybe it’s passed by reference because of side effects? Without comments or taking a look at the wider code, we will never know.
Lastly, using pointers causes the variable to escape to heap in the vast majority of the cases. In the hot path of large-scale systems, this could lead to a big number of allocations, which is both a waste and leads to detoriated performance vs using the stack.
2. Default or magic values
In idiomatic Go, zero (default) values are often used and aren’t carrying a special meaning, and doing otherwise leads to confusion.
They are also again, implicit. An index (an int
that is >=0) passed to a function will probably cause issues if it’s
passed as -1
in the cases where an element is “missing”, because people don’t expect it (yes, the standard library
does this in several places, but there the API is well-defined).
3. Accompanying bools
This is the first solution that does not have any problems except for the verbosity. But who wants to pass a bool for all optional values?
4. Functions with variadic arguments
This is specific to functions only, but you can use variadic arguments to represent optional values. There is also a very nice pattern called Options.
Generics to the rescue
With the introduction of generics in Go 1.18, we finally got some toys to play around with and experiment more. We can
leverage them to make one Optional
type that could wrap any other type without any external libs or codegen required.
Hot take: in my opinion, the usefulness of generics is seriously hindered by how the existing interface system worked and the fact that generic types can only be that — types — and not values like in C++ or other languages. Nevertheless, generics are still very useful in cases where you need to return a concrete type.
First, we need to define some goals for our type.
- Following the “Make the zero value useful” proverb, a zero-valued optional should be empty.
- It should be able to wrap any type, even pointers.
- We want it to be type and panic-safe.
- No allocations on the heap, where possible.
- Data can only be mutated through exposed API of the type.
Our type is going to be simple. We just store the data itself, and an accompanying bool that tells us whether it’s set or not. Both of them are going to be unexported and thus inaccessible from outside of the package, forcing users to only interact with them through receiver methods. Alternatively to the bool, we could just store a pointer to the data, but that would cause it to escape to the heap.
Let’s add the type itself in a self-contained package called optional
. I’m going to name it T
because I think
optional.T
rings better than optional.Optional
. This kind of messes things up in the sense that generic types are
also often called T
, but we can get around that.
|
|
This struct is all we are going to need. We can now add constructors. The obvious one here is the one that sets a value for our wrapper. But we can add convenience methods such as an empty constructor, to explicitly set an empty value, and a constructor with a pointer argument, where we set the data based on whether the argument is nil or not.
|
|
Interestingly, the pointer constructor still doesn’t escape to the heap. With the constructors done, we can now focus on adding different mutator and accessor methods.
|
|
The *new(Type)
calls still won’t escape since they’re not referenced anywhere outside the methods. They could also
be replaced by doing, for example the below, but I find the former solution cleaner.
|
|
We could also add other methods, such as a pointer accessor, that returns nil for empty optionals, making it seamless in database model mappers. We could add getters that return a zero value or an alternative value for empty optionals. The only limit here is your imagination :)
I’ve published the whole code here. You can make your own type based on it
or just use the type directly by calling
$ go get github.com/kristofaranyos/optional
.
Afterthoughts
I hope this was an informative article and you find this new type helpful. Don’t hesitate to reach out to me with any questions or thoughts!