Generic types in Go
From Know Go
Typing is no substitute for thinking.
—“A Manual for BASIC”
This is the third in a four-part series of tutorials on generics in Go, extracted from my book Know Go.
- Generics
- Type parameters
- Generic types
- Constraints
Generic functions are great, as we saw in the previous tutorial, but we can do more. We can also write generic types. What does that mean?
Generic types
We often deal with collections of values in Go. For example, consider this slice type:
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.
Defining a generic slice type
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
The elements all have the same type
It’s very important to understand 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.
For example, we can’t create a Bunch
of one type, and
then try to append a value of a different type:
:= Bunch[int]{1, 2, 3}
b = append(b, "hello")
b // cannot use "hello" (untyped string constant) as int value in
// argument to append
We can have a Bunch[int]
or a
Bunch[string]
or a Bunch
of elements
of any other specific type. What we can’t have is a Bunch
of mixed-type elements.
It’s easy to hear a term like “generic slice” and jump to the conclusion that it means “slice containing values of different types”. But that’s actually not the case.
Generic types need to be instantiated
There are, in a sense, no generic types in Go. I know that sounds crazy, but stick with me.
That is, you can define generic types like
Bunch[E]
, but to actually use them in your program, you
need to instantiate them on some specific type, like
int
.
At that point, what you have is an ordinary Go slice of
int
, and it stands to reason that such a slice can only
contain int
elements.
So just because we used a type parameter, it doesn’t mean we can
create a single slice that contains elements of different types. What we
can create are different slice types, such as
[]int
, or []string
, without having to specify
their element type in advance.
One way to express this is to say that, while we can define generic types at compile time, there are no generic types at run time.
There’s one partial exception to this, which you’re already familiar
with: interface types. We could always create a slice of
any
in Go, and populate it with elements of different
dynamic types. But, for the reasons we’ve discussed, this isn’t the same
thing as a truly generic type.
Exercise: Group therapy
Over to you again 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!
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!
There are no generic functions
Just as with generic types, then, there are no generic functions in Go, if you want to be a smart-alec about it (and I do).
Any generic functions you may write will in fact be instantiated on
some specific type at compile time, and they will then just be plain old
functions, like func(string) string
. If we wanted to give
such a specific function type a name, we could:
type Stringulator func(string) string
Could we define a generic function type, though? That is, give a name to the type of a generic function on some arbitrary type T.
For example, could we write something like:
type idFunc func[T any](T) T
// syntax error: function type must have no type parameters
Nope. If you think about it, how could Go compile this? It couldn’t, because all type arguments must be known at compile time.
What we can write is this:
type idFunc[T any] func(T) T
This is fine, because it’s a generic type, just like the
ones we’ve already seen. For some T, we’re saying, an
idFunc[T]
is a function that takes a T and returns a T.
If this is ever instantiated in our program, it will become some
specific type like func(int) int
. If not, Go can safely
ignore it. Either way, no unknown types are involved.
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
We saw in the previous tutorial
that 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. So
this is just the same kind of problem as when we tried to add together
two any
values.
If x
and y
were some struct type, for
example, that certainly wouldn’t work: structs don’t support
+
, because it’s not meaningful to add two structs together.
The +
operator isn’t defined on structs, we
say.
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.
It might be that, in a given program, we only ever instantiate
AddAnything
on types that do happen to support
+
, such as int
or string
. But
that’s not good enough for Go: we need to guarantee that it
can’t be instantiated on some inappropriate type.
To do this, 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.