What is this?
-
Go 1.18 introduces generics to the language
-
Some may just want to skim Go generics by example in a blog post
-
This is that blog post
(!) caution: Language pedants may find the content triggering
new terms
generics: The idea that type information can be determined not during implementation, but later
type parameter: The thing that represents potential types during implementation
constraint: The thing that places restrictions on a type parameter
the min example
Remember writing this over and over?
func min(a, b int) int {
if a < b {
return a
}
return b
}
Of course that only works for type int
. You’d have to implement the same function for
int64
, float64
, uint8
, … etc. Well generics can fix this. So let’s fix it.
We’ll need to add a type parameter to make the function generic. Which type parameter?
Well, one that lets us do the things we need to do with the type. We describe such a thing
as a constraint. In this case we need to be able to use the <
operator. Luckily, Go
comes with just the right tool for the job, constraints.Ordered
.
(!) caution: It seems the constraints
package may be moved
under exp/
for Go 1.18 final, indicating it is still experimental. Use at your own peril.
Okay lets add that.
func min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
Awesome! Our min
function is now generic and will work for any type that satisfies the
Ordered
type constraint. We’ll talk about custom constraints later.
the set example
Making a function generic is nice, but what about creating a generic data structure?
Let’s create a trivial set
implementation that lets us add items. Basically a re-usable wrapper
for the conventional abuse of map[<type>]struct{}
. We can then add some functions for basic set operations.
First we declare our parameterized type. Should we use any
, the drop-in replacement for interface{}
?
No, because any
does not allow use to use the ==
and !=
operators, which we’ll need for
comparing items in the set. Instead, we use the built-in comparable
constraint.
type Set[T comparable] struct {
m map[T]struct{}
}
Fantastic, we now have a generic Set
type. Let’s add a constructor so we can safely instantiate
that underlying map
. Notice how the NewSet
function is parameterized with the generic type
parameter we want, and that type information is used when instantiating the Set
.
func NewSet[T comparable]() *Set[T] {
return &Set[T]{
m: make(map[T]struct{}),
}
}
Now that we have a generic data structure, lets add some methods to make it useful. Most
sets contain elements, so how about an Insert
method. Because our Set
is generic for
[T comparable]
we should parameterize our Insert
method to accept only elements of
type T
accordingly.
func (s *Set[T]) Insert(elem T) {
s.m[elem] = struct{}{}
}
That’s it! You can imagine implementing all the other usual set operations in a similar
fashion. Do not be afraid of using Set[T]
as a method parameter type or return type,
it all works the same. This might be the signature for a Union
method, for example.
func (s *Set[T]) Union(o *Set[T]) *Set[T] {
// ...
}
the custom constraint example
So far we have only used constraint
types offered by Go (constraints.Ordered
and comparable
).
What if we want a custom constraint? Like if we want something that only accepts types
that implement a Pretty() string
function. For that we could create our custom constraint,
type Prettier interface {
Pretty() string
}
Now we can use our interface
as a type constraint for type parameters on our generic functions
or structs. Here is a PrettyPrint
function that accepts a slice of Prettier
, calling Pretty
on each
element. (Unlike before generics, when we would have to create a new slice of type []Prettier
, convert
each element, etc…)
func PrettyPrint[T Prettier](items []T) {
for _, item := range items {
fmt.Println(item.Pretty())
}
}
more custom constraint syntax
Constraints can be used to describe the union
of more than one type. For example, we might
want a constraint allowing only the 8-bit integer types byte
, int8
, and uint8
. To
do this we use the |
(union) operator.
type ByteSize interface {
byte | uint8 | int8
}
constraints can be generic
Constraints themselves can be declared generically.
type Bar interface {
Bar()
}
type Foo[B Bar] interface {
Foo[B]()
}
TBH I am not sure what to do with that.
the end
Congrats, you now know enough about Go generics to write some reusable code. But please remember, with great power comes great responsibility. I do not hope to see Go2EE become a thing.
gl;hf!