Type parameters in Go
I learned very early the difference between knowing the name of something, and knowing something.
—Richard Feynman, “The World From Another Point of View”
This is the second in a four-part series of tutorials on generics in Go.
- Generics
- Type parameters
- Generic types
- Constraints
Now that we’ve described what generic programming is, and how it came to Go, it’s time to look at exactly what Go’s generics support involves, and what we can do with it. It’s a big subject: so much so that I’ve written a whole book exploring it, called Know Go. What follows is just a little taster of the content in the book!
Generic functions
In the previous post, we looked at non-generic ways to write
functions like PrintAnything
and AddAnything
,
using interfaces. And we found that while interfaces give us limited
support for polymorphism, those limits can be frustrating. If
we use the any
interface, which as the name suggests
matches any type, then there’s very little we can do with the resulting
values. We can’t even add them together, for example.
And if we use more restrictive interfaces, such as
io.Reader
, we can only specify a set of types that have
certain methods (for example, Read
). That’s useful
in many cases, but there are lots of types that we can’t include in this
way, because they have no methods (all the built-in types, for
example).
To write polymorphic functions on these types, and to specify sets of types in more flexible ways, we need generics. And now we have exactly that, so let’s see what we can use it to do.
Introducing T, the arbitrary type
With Go generics, we can write generic functions: ones that take parameters not of some specific named type, but of some arbitrary type that we don’t have to specify in advance. Let’s call it T, short for “type”.
When the function is actually called in our program, T will
be some specific type, such as int
. But we don’t want to
have to specify that in advance when we’re writing the function, so
we’re just going to use T as a placeholder for whatever the type ends up
being.
In fact, there might be more than one T in our finished program. For
example, we might call our generic function with a float64
value, in which case T will be float64
. But elsewhere we
might call the same function with a string
value, in which
case T will be string
.
This T placeholder is called a type parameter. A generic function in Go, then, is a function that takes a type parameter. So what does that look like?
Let’s take the simplest imaginable case first. We’ll write a
PrintAnything
function with a type parameter T, where T is
a placeholder for any type:
func PrintAnything[T any](v T) {
Type parameters
What does this mean? For any type T, PrintAnything[T]
takes a T parameter (that is, a parameter whose type is T), and returns
nothing. The function signature just says that in Go instead of
English.
While v
is just an ordinary parameter, of some
unspecified type, T is different. T is a new kind of parameter in Go: a
type parameter.
We say that PrintAnything
is a parameterised
function, that is, a generic function on some type T. For
short, we usually just talk about PrintAnything[T]
,
pronounced “PrintAnything
of T”.
Instantiation
What type is T, specifically? It depends what we decide to pass to the function when it’s called.
Suppose we want to pass it an int
value, for
example:
var x int = 5
(x)
PrintAnything// Output:
// 5
Go is smart enough to know that x
is an
int
, and therefore it needs to compile (and call) a version
of PrintAnything[T]
where T is int
.
This is called instantiating the function, as in “creating
an instance of it”. In effect, the generic PrintAnything
function is like a kind of template, and when we call it with some
specific type, we create a specific instance of the function
that takes that type—for example, int
.
What if we have another call to PrintAnything[T]
somewhere else in the program, and this time T is a different type, such
as string
? Well, that’s okay. Go will produce another
version of PrintAnything
, this time one that takes a string
argument.
In most cases, as in these examples, Go can infer the type of T from the value that’s supplied at the site of the function call. When that isn’t possible, Go will let you know.
For example, suppose we have a generic function
Something[T any]
that returns a value of T. And
suppose we call it like this:
:= Something() x
What is T here? We don’t know, and nor does Go, so it must complain:
cannot infer T
All is not lost, however. We can specify which particular T we want in this case by putting it inside square brackets after the function name:
:= Something[int]() x
It’s always okay to explicitly instantiate a parameterised function or type in this way. But you only have to do it when Go can’t automatically infer the required type, which isn’t all that often.
Stencilling
This approach to implementing generics is sometimes called stencilling, which is a rather apt name. You can imagine Go spray-painting a bunch of similar versions of the function, differing only in the type of the parameter they take.
We could have done the same thing ourselves using the existing code generation machinery in Go, and indeed many people did do exactly that before the introduction of generics.
This makes for efficient machine code, because there’s no indirection
(unlike with interface values). We don’t need type assertions, because
each different implementation of PrintAnything
knows
exactly what concrete type it’s getting.
This isn’t a particularly compelling example of generics in action,
though, because it was already possible to write
PrintAnything
using a method-set interface:
any
. We could just pass the argument straight to
fmt.Println
, which also takes any
.
Getting started
Let’s look at a slightly more interesting, though still rather contrived, example.
An Identity
function
Suppose we want to write a function called Identity
that
simply returns whatever value you pass it. (I know it doesn’t sound very
useful, but bear with me.) How could we write such a function in Go,
without having to implement a separate version for each possible type of
value?
This is where we start to go beyond the limits of interfaces. Using
any
, for example, we’d have to write something like:
func Identity(v any) any {
return v
}
This works, but it isn’t really satisfactory. As we saw with
AddAnything
in the previous
tutorial, we don’t have any way to tell Go that the function’s
parameter and its result must be the same concrete type,
whatever it is. And clearly that needs to be the case here: if we pass
this function an int
, we expect to get an int
back.
Now we know how to specify that requirement, using a type parameter:
func Identity[T any](v T) T {
return v
}
Notice that, although it looks similar to the non-generic version, there’s an important difference. Whatever T turns out to be (and it can be any type), both the parameter and the function’s result must be of that type.
Instantiating Identity
Suppose we call this function somewhere in our program with a string argument, then:
.Println(Identity("Hello"))
fmt// Output:
// Hello
You now know how this works. Under the hood, Go instantiates a
version of Identity
that takes a string parameter and
returns a string result. This is just a plain, ordinary Go function that
we could have written ourselves, or generated mechanically.
The point is, of course, that we don’t need to supply a
separate version of Identity
for each concrete type that we
want to use. Instead, we just write it once for some arbitrary type T,
and Go will automatically generate a version of Identity
for each type that’s actually used in our program.
Exercise: Hello, generics
Now it’s over to you to write your first generic function in Go! Let’s work through it together, step by step.
First of all, make sure you’ve checked out a copy of the GitHub repo for the Know Go book:
Open the exercises/print
folder in your code editor and take a look at the print_test.go
file.
You’ll find this test:
func TestPrintAnythingTo_PrintsToGivenWriter(t *testing.T) {
.Parallel()
t:= new(bytes.Buffer)
buf print.PrintAnythingTo(buf, "Hello, world")
:= "Hello, world\n"
want := buf.String()
got if want != got {
.Errorf("want %q, got %q", want, got)
t}
}
Running the test
Run the test using your editor, or the go test
command.
You’ll see that right now the test doesn’t compile, because the required
function doesn’t exist:
undefined: print.PrintAnythingTo
Remember, you’ll need at least Go version 1.18 to be able to use generics, including running this test and implementing the function to make it pass. That’s because we’re using some new syntax that doesn’t exist in Go version 1.17 and earlier.
If you try to compile this code with an older version of Go that doesn’t support generics, you’ll get a rather confusing additional error message:
type string is not an expression
So if you see this, you need to upgrade your Go (if possible, to the current stable version). Then read on.
GOAL: Get the test passing!
HINT: To make this test even compile, you’ll need to
define a generic function in the print
package named
PrintAnythingTo
that takes one parameter of type
io.Writer
, and another value that’s of some unspecified
type.
In other words, PrintAnythingTo
has a type parameter
we’ll refer to as T, which can be any type, and it takes a parameter of
this type T, just like Identity
. But unlike
Identity
, it also takes another parameter, which is of type
io.Writer
.
To make the test pass, your function will need to write the
supplied value to the supplied writer. It’s up to you how to do this,
but you might like to use fmt.Fprintln
, like our
PrintTo
example in the previous
tutorial.
The necessary go.mod
and print.go
files are
already set up for you. All you need to do is edit print.go
and add the PrintAnythingTo
function, then run the test
again.
SOLUTION: Here’s my suggested solution; it’s fine if yours looks different, so long as it passes the test. It’s just for comparison, or if you need a little extra help on this tricky first exercise.
package print
import (
"fmt"
"io"
)
func PrintAnythingTo[T any](w io.Writer, p T) {
.Fprintln(w, p)
fmt}
We use the [T any]
syntax to say that
PrintAnything
is about some arbitrary type T, and it takes
a parameter—p
, the thing to print—of that type, whatever it
actually turns out to be. In the test, as it happens, that’s a string,
but it could have been any type. If you like, you can add more
tests that call PrintAnything
with different types, such as
int
or float64
.
Composite types
So far, we’ve figured out how to define a generic function that, for some type T, takes a parameter of type T:
func Identity[T any](v T) {
So is that it? Are we restricted to declaring only parameters of type T itself, or could we also take some composite type? That is, some type involving T, not just T itself? For example, a slice of T?
Slices of some arbitrary type
We could indeed. Suppose we wanted to write a function
Len
that returns the length of a given slice. And its
parameter will always be a slice of something: that is, of some
arbitrary element type. Let’s call it E, for “element”.
The signature of Len
, then, might look something like
this:
func Len[E any](s []E) int {
It’s conventional, though not required, to capitalise the names of type parameters. Those names can be whatever you like, but again it’s conventional to use a single letter: T for any type, E for an element type of a slice, and so on.
Other generic composite types
But we can also use a type parameter in other kinds of composite type. For example, we can write a generic function on a channel of some element type E:
func Drain[E any](ch <-chan E) {}
We could even write a variadic function: that is, a function that takes a variable number of arguments. In this case, it would take a variable number of channels of E:
func Merge[E any](chs ...<-chan E) <-chan E {
In the next post, we’ll talk more about generic types in Go, and look at how we can use type parameters to create our own generic collection types, for example.