The Long-Awaited Go Feature: Generics
A short guide to use generics in Go
Since its birth, Go has been praised for a lot of reasons but criticized for others, being the main pain point of the topic of this story: Generics. Or rather the lack of them.
First of all, the reason behind this decision was to keep the language simple, as it is Go’s philosophy.
From Go’s FAQ:
Go was intended as a language for writing server programs that would be easy to maintain over time. (See this article for more background.) The design concentrated on things like scalability, readability, and concurrency. Polymorphic programming did not seem essential to the language’s goals at the time, and so was left out for simplicity.
[…]
Generics are convenient but they come at a cost in complexity in the type system and run-time. We haven’t yet found a design that gives value proportionate to the complexity, although we continue to think about it. Meanwhile, Go’s built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.
What are Generics?
Sometimes it’s useful to implement data structures or algorithms that can work with any data type, such as a function that performs an operation with every element of an array, regardless of the data type. For example, if we want to iterate over an array of elements, print on the console each one — until now, the only way to do something like that was this:
func Print(arr []interface{}) {for _, elem := range arr {
switch o := i.(type) {
case int64:
fmt.Printf("%5d\n", o)
case float64:
fmt.Printf("%7.3f\n", o)
case string:
fmt.Printf("%s\n", o)
default: // covers structs and such
fmt.Printf("%+v\n", o)
}
}
}
Not very practical, in my humble opinion.
Generics has been the top requested feature to Go development team, as shown in the following chart extracted from the Go 2020 Survey:
Requests have been heard and Generics are being released in version 1.18 (expected to come out February 2022).
That doesn’t mean we can’t have fun until then. The release date is for production-ready versions and we now have access to the beta 2 (you can learn how to install it here). Without further ado, let’s get into Go Generics!
Defining a generic function
To define a generic function we have to place the generic type identifier followed by the word any
between square brackets, after the function name and before the parameters.
This way, we can pass any slice with any datatype and it will get printed. Let’s try with a slice of int, another one of string and a final one with a struct.
And we get the following console output:
1
2
3
4
hello
i'm
using
generics
{Matthew}
{John}
As we can see, every data type got printed with the same function. Yay!
Constraints
There’s a way to limit what data types our generic function can admit by using constraints. For example, we may want to limit the admitted types only to data types that can be converted to strings. For this, we create an interface Stringer
with the function String() string
and make the generic datatype T Stringer
. This way, the function will only accept types that implement the String() string
function.
Console output:
Matthew Johnson
John Johnson
Comparable Constraint
We can declare a comparable
generic type to restrict parameters to data types that can be used with the operands ==
and !=
. Let’s make a generic function that counts occurrences of a value in an array and execute it with an array of string and then with an array of int:
Console output:
3
2
2
3
Several types
We can declare a function that accepts only certain data types:
Console output:
6
14.3
We can also declare an interface with a list of data types:
type Addable type {
int | int32 | int64 | float32 | float64 | uint | uint32 | uint64
}
But if we perform an addition between an Addable data type and an alias
type:
We get the following error:
./main.go:17:17: myInt does not implement Addable (possibly missing ~ for int in constraint Addable)
As the error message says, we can resolve this by simply adding ~
before the int
data type declaration in the Addable
interface. This way any alias type of int
implements Addable. This applies to any alias we want to implement Addable. For exmaple, if we’d want an alias of float64
to implement Addable we should add ~
before the float64
data type declaration.
And now it works:
9
Mixed
Constraints can also be mixed together in a new interface
type ComparableStringer interface {
comparable
String() string
}
This way, a generic data type T ComparableStringer
will only accept types that implement the String() string
method and are comparable
.
I left out some topics regarding Go Generics to make this a light read, so make sure you keep on learning about them!
Happy coding!