Generic types in Go
From Know Go
Typing is no substitute for thinking.
—“A Manual for BASIC”
Just as you enter the airport security line, a sinister-looking figure slips a package into your hand. “Could you just take this on the flight for me?” Sure, you think to yourself. What kind of idiot would carry a package for someone when they’ve absolutely no idea what’s inside it?
Well, generics in Go are exactly like that, only in a good way. It turns out we quite often want to be able to deal with collections of values in Go programs without knowing—or caring—what kind of values they are. In fact, unlike your carry-on baggage, the less we know about them the better.
For example, consider a slice type like this:
type SliceOfInt []int
You already know that, just as an int
variable can only
hold int
values, a []int
slice can only hold
int
elements. We wouldn’t want to have to also define
SliceOfFloat
, SliceOfString
, and so on.
As we saw in Type parameters, Go lets us hand-wave these details away by referring to some arbitrary type (“T”, for example, like our sinister “Mr X” at the airport). We can then just use the label T, or whatever, instead of tediously listing all the specific types that we might want to handle in a function.
So, could we write a generic type definition that takes a type parameter, just like a generic function? For example, could we make a slice of any type?
Yes, we could:
type Bunch[E any] []E
Just as we could define a function such as Len
that
takes a slice of an arbitrary element type, we’ve now given a name to a
new type: a slice of E, for some type E.
And just as with generic functions, a generic type is always
instantiated on some specific type when it’s used in a program.
That is to say, a Bunch[int]
will be a slice of
int
, a Bunch[string]
will be a slice of
strings, and so on.
Each of these is a distinct concrete type, as you’d expect, and we
can write a Bunch
literal by giving the type we want in
square brackets:
:= Bunch[int]{1, 2, 3} b
One important thing to remember here is that, even though a
Bunch
is defined as a slice of E for any type E, any
particular Bunch
can’t contain values of
different types. All the elements of a Bunch[T]
must be of type T, whatever it is.
Exercise: Group therapy
Over to you now, to try your hand at the group
exercise. This time, you’ll need to define a generic slice type
Group[E]
to pass the following test:
func TestGroupContainsWhatIsAppendedToIt(t *testing.T) {
.Parallel()
t:= group.Group[string]{}
got = append(got, "hello")
got = append(got, "world")
got := group.Group[string]{"hello", "world"}
want if !slices.Equal(want, got) {
.Errorf("want %v, got %v", want, got)
t}
}
GOAL: Get the test passing!
HINT: Well, a “group” is a lot like a “bunch”, and I don’t think you’ll need any more hints than that. There’s no trick here: it’s just a confidence builder to get you used to defining generic types.
SOLUTION: And here’s what that looks like, just as
we saw before with the Bunch
type:
type Group[E any] []E
I told you it was easy! My book Know Go uses little exercises like this to get you familiar and confident working with Go generics, iterators, and other useful features of modern Go. In this excerpt, we’ll see what else you can do with generic types.
Generic function types
You probably know that functions are values in Go. That is, you can pass a function as an argument to another function, you can return a function from a function, and you can declare variables or struct fields of a particular function type.
In other words, just as Go values can be integers, floats, or
strings, they can also be functions. For example, a function that takes
an int
parameter and returns bool
would have
the type func(int) bool
. This is a very handy feature of
Go, and we use it a lot.
Generic functions as values
So what if that function were generic? For example, the
Identity[T]
function in our earlier example. How would we
declare a variable to which we could assign the function
Identity[T]
? What would be the type of such a variable?
Well, you now know that there’s actually no such function as
Identity[T]
, only one or more instantiations of
that function on a specific type, such as string
. As we’ve
seen, the Identity[T]
function definition is just a kind of
“stencil” that Go uses to produce actual functions.
So there is such a function as
Identity[string]
, and accordingly we can use it as a value.
For example, we can assign it to a variable:
:= Identity[string] f
The type is always instantiated
What is the type of the variable f
, then? Well, it’s
whatever the type of Identity[string]
is. Here’s the
signature of the generic Identity
function again:
func Identity[T any](v T) T {
So if T is string
in this case, then we feel the type of
Identity[string]
should be:
func(string) string
Let’s ask Go itself. Conveniently, the fmt
package can
report the type of a value, using the %T
verb. So we’ll see
what type it thinks f
is in this case:
:= Identity[string]
f .Printf("%T\n", f)
fmt// Output:
// func(string) string
How straightforward!
Generic types as function parameters
We can create both generic functions and generic types, as we’ve seen. So an interesting question occurs: could we write a generic function that takes a parameter of a generic type?
The answer is absolutely yes:
func PrintBunch[E any](v Bunch[E]) {
.Println(v)
fmt}
func main() {
:= Bunch[int]{1, 2, 3}
b (b)
PrintBunch// Output:
// [1, 2, 3]
}
This is pretty exciting stuff! But there’s a limit to what we can do with generic functions that take literally any type. We’ll see what that limit is in a moment, but first, let’s do a little more confidence building.
Exercise: Lengthy proceedings
Now it’s your turn to write a generic function on a generic type, to
solve the length
exercise.
func TestLenOfSliceIs2WhenItContains2Elements(t *testing.T) {
.Parallel()
t:= []int{1, 2}
s := 2
want := length.Len(s)
got if want != got {
.Errorf("Len(%v): want %d, got %d", s, want, got)
t}
}
GOAL: Your task is to implement a generic function
Len[E]
that returns the length of a given slice. As usual,
the test will tell you when you’ve got it right!
HINT: Actually, the implementation is easy, isn’t it? We don’t need to write code to figure out the length of some slice; there’s a built-in function for that (see if you can guess which one).
The tricky bit, if it is tricky, might be writing the
signature of the Len
function. But it’s not really
any more complicated than what we’ve seen already.
For example, the PrintBunch
function takes a
Bunch[E]
for some arbitrary element type E. Well, so does
this one! Can you see what to do?
SOLUTION: Here’s one possible answer:
func Len[E any](s []E) int {
return len(s)
}
Constraining type parameters
As we saw in Type parameters,
one of the limitations of interface types in Go is that we can’t use
them with operators such as +
. Remember our
AddAnything
example?
We can’t add any
to
any
You might be wondering if the same kind of limitation applies to
functions parameterised by T any
, and indeed it does. If we
try to write a generic AddAnything[T any]
, for example,
that doesn’t work:
func AddAnything[T any](x, y T) T {
return x + y
}
// invalid operation: operator + not defined on x (variable
// of type T constrained by any)
Go is always right, but it can sometimes express itself in a rather terse way, so let’s unpack that error message a little.
It’s complaining about this line:
return x + y
And it’s saying:
operator + not defined on x
To put it another way, Go has no way to guarantee that whatever
specific type T happens to be when the function is instantiated, that
type will work with the +
operator.
It might, but then again, it might not, because:
variable of type T constrained by any
Not every type is “addable”
In other words, because x
can be any type,
there’s no way to guarantee that, when the program runs, x
will be one of the types that supports the +
operator. Go
is telling us that we can’t write the expression x + y
with
values of literally any type T: that’s too broad a range of
possible types.
To solve this problem, we need to constrain T a little more. That is, to restrict the allowed possibilities for T to only those types that support the operator we want to use. And we’ll see how to do that in the next post, which is about constraints.