Generic types in Go

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:

b := Bunch[int]{1, 2, 3}

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) {
    t.Parallel()
    got := group.Group[string]{}
    got = append(got, "hello")
    got = append(got, "world")
    want := group.Group[string]{"hello", "world"}
    if !slices.Equal(want, got) {
        t.Errorf("want %v, got %v", want, got)
    }
}

(Listing exercises/group)

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

(Listing solutions/group)

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:

f := Identity[string]

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:

f := Identity[string]
fmt.Printf("%T\n", f)
// 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]) {
    fmt.Println(v)
}

func main() {
    b := Bunch[int]{1, 2, 3}
    PrintBunch(b)
    // 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) {
    t.Parallel()
    s := []int{1, 2}
    want := 2
    got := length.Len(s)
    if want != got {
        t.Errorf("Len(%v): want %d, got %d", s, want, got)
    }
}

(Listing exercises/length)

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)
}

(Listing solutions/length)

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.

Constraints in Go

Constraints in Go

Testing legacy code

Testing legacy code

0