Type parameters in Go

Type parameters in Go

From Know 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.

  1. Generics
  2. Type parameters
  3. Generic types
  4. 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
PrintAnything(x)
// 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:

x := Something()

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:

x := Something[int]()

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:

fmt.Println(Identity("Hello"))
// 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) {
    t.Parallel()
    buf := new(bytes.Buffer)
    print.PrintAnythingTo(buf, "Hello, world")
    want := "Hello, world\n"
    got := buf.String()
    if want != got {
        t.Errorf("want %q, got %q", want, got)
    }
}

(Listing exercises/print)

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) {
    fmt.Fprintln(w, p)
}

(Listing solutions/print)

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.

Programming with confidence: TDD in Go

Programming with confidence: TDD in Go

Generics in Go

Generics in Go

0