| --- |
| title: An Introduction To Generics |
| date: 2022-03-22 |
| by: |
| - Robert Griesemer |
| - Ian Lance Taylor |
| tags: |
| - go2 |
| - generics |
| summary: An introduction to generics in Go. |
| --- |
| |
| ## Introduction |
| |
| This blog post is based on our talk at GopherCon 2021: |
| |
| {{video "https://www.youtube.com/embed/Pa_e9EeCdy8"}} |
| |
| The Go 1.18 release adds support for generics. |
| Generics are the biggest change we've made to Go since the first open |
| source release. |
| In this article we'll introduce the new language features. |
| We won't try to cover all the details, but we will hit all the |
| important points. |
| For a more detailed and much longer description, including many |
| examples, see the [proposal |
| document](https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md). |
| For a more precise description of the language changes, see the |
| [updated language spec](/ref/spec). |
| (Note that the actual 1.18 implementation imposes some restrictions on |
| what the proposal document permits; the spec should be accurate. |
| Future releases may lift some of the restrictions.) |
| |
| Generics are a way of writing code that is independent of the specific |
| types being used. |
| Functions and types may now be written to use any of a set of types. |
| |
| Generics add three new big things to the language: |
| 1. Type parameters for function and types. |
| 2. Defining interface types as sets of types, including types that |
| don't have methods. |
| 3. Type inference, which permits omitting type arguments in many |
| cases when calling a function. |
| |
| ## Type Parameters |
| |
| Functions and types are now permitted to have type parameters. |
| A type parameter list looks like an ordinary parameter list, except |
| that it uses square brackets instead of parentheses. |
| |
| To show how this works, let's start with the basic non-generic `Min` |
| function for floating point values: |
| |
| {{raw ` |
| func Min(x, y float64) float64 { |
| if x < y { |
| return x |
| } |
| return y |
| } |
| `}} |
| |
| We can make this function generic--make it work for different |
| types--by adding a type parameter list. |
| In this example we add a type parameter list with a single type |
| parameter `T`, and replace the uses of `float64` with `T`. |
| |
| {{raw ` |
| import "golang.org/x/exp/constraints" |
| |
| func GMin[T constraints.Ordered](x, y T) T { |
| if x < y { |
| return x |
| } |
| return y |
| } |
| `}} |
| |
| It is now possible to call this function with a type argument by |
| writing a call like |
| |
| {{raw ` |
| x := GMin[int](2, 3) |
| `}} |
| |
| Providing the type argument to `GMin`, in this case `int`, is called |
| _instantiation_. |
| Instantiation happens in two steps. |
| First, the compiler substitutes all type arguments for their |
| respective type parameters throughout the generic function or type. |
| Second, the compiler verifies that each type argument satisfies the |
| respective constraint. |
| We'll get to what that means shortly, but if that second step fails, |
| instantiation fails and the program is invalid. |
| |
| After successful instantiation we have a non-generic function that can |
| be called just like any other function. |
| For example, in code like |
| |
| {{raw ` |
| fmin := GMin[float64] |
| m := fmin(2.71, 3.14) |
| `}} |
| |
| the instantiation `GMin[float64]` produces what is effectively our |
| original floating-point `Min` function, and we can use that in a |
| function call. |
| |
| Type parameters can be used with types also. |
| |
| {{raw ` |
| type Tree[T interface{}] struct { |
| left, right *Tree[T] |
| value T |
| } |
| |
| func (t *Tree[T]) Lookup(x T) *Tree[T] { ... } |
| |
| var stringTree Tree[string] |
| `}} |
| |
| Here the generic type `Tree` stores values of the type parameter `T`. |
| Generic types can have methods, like `Lookup` in this example. |
| In order to use a generic type, it must be instantiated; |
| `Tree[string]` is an example of instantiating `Tree` with the type |
| argument `string`. |
| |
| ## Type sets |
| |
| Let's look a bit deeper at the type arguments that can be used to |
| instantiate a type parameter. |
| |
| An ordinary function has a type for each value parameter; that type |
| defines a set of values. |
| For instance, if we have a `float64` type as in the non-generic |
| function `Min` above, the permissible set of argument values is the |
| set of floating-point values that can be represented by the `float64` |
| type. |
| |
| Similarly, type parameter lists have a type for each type parameter. |
| Because a type parameter is itself a type, the types of type |
| parameters define sets of types. |
| This meta-type is called a _type constraint_. |
| |
| In the generic `GMin`, the type constraint is imported from the |
| [constraints package](https://golang.org/x/exp/constraints). |
| The `Ordered` constraint describes the set of all types with values |
| that can be ordered, or, in other words, compared with the {{" < "}} |
| operator (or {{" <= "}}, {{" > "}}, etc.). |
| The constraint ensures that only types with orderable values can be |
| passed to `GMin`. |
| It also means that in the `GMin` function body values of that type |
| parameter can be used in a comparison with the {{" < "}} operator. |
| |
| In Go, type constraints must be interfaces. |
| That is, an interface type can be used as a value type, and it can |
| also be used as a meta-type. |
| Interfaces define methods, so obviously we can express type |
| constraints that require certain methods to be present. |
| But `constraints.Ordered` is an interface type too, and the {{" < "}} |
| operator is not a method. |
| |
| To make this work, we look at interfaces in a new way. |
| |
| Until recently, the Go spec said that an interface defines a method |
| set, which is roughly the set of methods enumerated in the interface. |
| Any type that implements all those methods implements that interface. |
| |
| {{image "intro-generics/method-sets.png"}} |
| |
| But another way of looking at this is to say that the interface |
| defines a set of types, namely the types that implement those methods. |
| From this perspective, any type that is an element of the interface's |
| type set implements the interface. |
| |
| {{image "intro-generics/type-sets.png"}} |
| |
| The two views lead to the same outcome: For each set of methods we can |
| imagine the corresponding set of types that implement those methods, |
| and that is the set of types defined by the interface. |
| |
| For our purposes, though, the type set view has an advantage over the |
| method set view: we can explicitly add types to the set, and thus |
| control the type set in new ways. |
| |
| We have extended the syntax for interface types to make this work. |
| For instance, `interface{ int|string|bool }` defines the type set |
| containing the types `int`, `string`, and `bool`. |
| |
| {{image "intro-generics/type-sets-2.png"}} |
| |
| Another way of saying this is that this interface is satisfied by |
| only `int`, `string`, or `bool`. |
| |
| Now let's look at the actual definition of `constraints.Ordered`: |
| |
| {{raw ` |
| type Ordered interface { |
| Integer|Float|~string |
| } |
| `}} |
| |
| This declaration says that the `Ordered` interface is the set of all |
| integer, floating-point, and string types. |
| The vertical bar expresses a union of types (or sets of types in this |
| case). |
| `Integer` and `Float` are interface types that are similarly defined |
| in the `constraints` package. |
| Note that there are no methods defined by the `Ordered` interface. |
| |
| For type constraints we usually don't care about a specific type, such |
| as `string`; we are interested in all string types. |
| That is what the `~` token is for. |
| The expression `~string` means the set of all types whose underlying |
| type is `string`. |
| This includes the type `string` itself as well as all types declared |
| with definitions such as `type MyString string`. |
| |
| Of course we still want to specify methods in interfaces, and we want |
| to be backward compatible. |
| In Go 1.18 an interface may contain methods and embedded interfaces |
| just as before, but it may also embed non-interface types, unions, and |
| sets of underlying types. |
| |
| When used as a type constraint, the type set defined by an interface |
| specifies exactly the types that are permitted as type arguments for |
| the respective type parameter. |
| Within a generic function body, if the type of a operand is a type |
| parameter `P` with constraint `C`, operations are permitted if they |
| are permitted by all types in the type set of `C` (there are currently |
| some implementation restrictions here, but ordinary code is unlikely |
| to encounter them). |
| |
| Interfaces used as constraints may be given names (such as `Ordered`), |
| or they may be literal interfaces inlined in a type parameter list. |
| For example: |
| |
| {{raw ` |
| [S interface{~[]E}, E interface{}] |
| `}} |
| |
| Here `S` must be a slice type whose element type can be any type. |
| |
| Because this is a common case, the enclosing `interface{}` may be |
| omitted for interfaces in constraint position, and we can simply |
| write: |
| |
| {{raw ` |
| [S ~[]E, E interface{}] |
| `}} |
| |
| Because the empty interface is common in type parameter lists, and in |
| ordinary Go code for that matter, Go 1.18 introduces a new predeclared |
| identifier `any` as an alias for the empty interface type. |
| With that, we arrive at this idiomatic code: |
| |
| {{raw ` |
| [S ~[]E, E any] |
| `}} |
| |
| Interfaces as type sets is a powerful new mechanism and is key to |
| making type constraints work in Go. |
| For now, interfaces that use the new syntactic forms may only be used |
| as constraints. |
| But it's not hard to imagine how explicitly type-constrained |
| interfaces might be useful in general. |
| |
| ## Type inference |
| |
| The last new major language feature is type inference. |
| In some ways this is the most complicated change to the language, but |
| it is important because it lets people use a natural style when |
| writing code that calls generic functions. |
| |
| ### Function argument type inference |
| |
| With type parameters comes the need to pass type arguments, which can |
| make for verbose code. |
| Going back to our generic `GMin` function: |
| |
| {{raw ` |
| func GMin[T constraints.Ordered](x, y T) T { ... } |
| `}} |
| |
| the type parameter `T` is used to specify the types of the ordinary |
| non-type arguments `x`, and `y`. |
| As we saw earlier, this can be called with an explicit type argument |
| |
| {{raw ` |
| var a, b, m float64 |
| |
| m = GMin[float64](a, b) // explicit type argument |
| `}} |
| |
| In many cases the compiler can infer the type argument for `T` from |
| the ordinary arguments. |
| This makes the code shorter while remaining clear. |
| |
| {{raw ` |
| var a, b, m float64 |
| |
| m = GMin(a, b) // no type argument |
| `}} |
| |
| This works by matching the types of the arguments `a` and `b` with the |
| types of the parameters `x`, and `y`. |
| |
| This kind of inference, which infers the type arguments from the types |
| of the arguments to the function, is called _function argument type |
| inference_. |
| |
| Function argument type inference only works for type parameters that |
| are used in the function parameters, not for type parameters used only |
| in function results or only in the function body. |
| For example, it does not apply to functions like `MakeT[T any]() T`, |
| that only uses `T` for a result. |
| |
| ### Constraint type inference |
| |
| The language supports another kind of type inference, _constraint type |
| inference_. |
| To describe this, let's start with this example of scaling a slice of |
| integers: |
| |
| {{raw ` |
| // Scale returns a copy of s with each element multiplied by c. |
| // This implementation has a problem, as we will see. |
| func Scale[E constraints.Integer](s []E, c E) []E { |
| r := make([]E, len(s)) |
| for i, v := range s { |
| r[i] = v * c |
| } |
| return r |
| } |
| `}} |
| |
| This is a generic function that works for a slice of any integer |
| type. |
| |
| Now suppose that we have a multi-dimensional `Point` type, where each |
| `Point` is simply a list of integers giving the coordinates of the |
| point. |
| Naturally this type will have some methods. |
| |
| {{raw ` |
| type Point []int32 |
| |
| func (p Point) String() string { |
| // Details not important. |
| } |
| `}} |
| |
| Sometimes we want to scale a `Point`. |
| Since a `Point` is just a slice of integers, we can use the `Scale` |
| function we wrote earlier: |
| |
| {{raw ` |
| // ScaleAndPrint doubles a Point and prints it. |
| func ScaleAndPrint(p Point) { |
| r := Scale(p, 2) |
| fmt.Println(r.String()) // DOES NOT COMPILE |
| } |
| `}} |
| |
| Unfortunately this does not compile, failing with an error like |
| `r.String undefined (type []int32 has no field or method String)`. |
| |
| The problem is that the `Scale` function returns a value of type `[]E` |
| where `E` is the element type of the argument slice. |
| When we call `Scale` with a value of type `Point`, whose underlying |
| type is `[]int32`, we get back a value of type `[]int32`, not type |
| `Point`. |
| This follows from the way that the generic code is written, but it's |
| not what we want. |
| |
| In order to fix this, we have to change the `Scale` function to use a |
| type parameter for the slice type. |
| |
| {{raw ` |
| // Scale returns a copy of s with each element multiplied by c. |
| func Scale[S ~[]E, E constraints.Integer](s S, c E) S { |
| r := make(S, len(s)) |
| for i, v := range s { |
| r[i] = v * c |
| } |
| return r |
| } |
| `}} |
| |
| We've introduced a new type parameter `S` that is the type of the |
| slice argument. |
| We've constrained it such that the underlying type is `S` rather than |
| `[]E`, and the result type is now `S`. |
| Since `E` is constrained to be an integer, the effect is the same as |
| before: the first argument has to be a slice of some integer type. |
| The only change to the body of the function is that now we pass `S`, |
| rather than `[]E`, when we call `make`. |
| |
| The new function acts the same as before if we call it with a plain |
| slice, but if we call it with the type `Point` we now get back a value |
| of type `Point`. |
| That is what we want. |
| With this version of `Scale` the earlier `ScaleAndPrint` function will |
| compile and run as we expect. |
| |
| But it's fair to ask: why is it OK to write the call to `Scale` |
| without passing explicit type arguments? |
| That is, why can we write `Scale(p, 2)`, with no type arguments, |
| rather than having to write `Scale[Point, int32](p, 2)`? |
| Our new `Scale` function has two type parameters, `S` and `E`. |
| In a call to `Scale` not passing any type arguments, function argument |
| type inference, described above, lets the compiler infer that the type |
| argument for `S` is `Point`. |
| But the function also has a type parameter `E` which is the type of the |
| multiplication factor `c`. |
| The corresponding function argument is `2`, and because `2` is an _untyped_ |
| constant, function argument type inference cannot infer the correct type for |
| `E` (at best it might infer the default type for `2` which is `int` and which |
| would be incorrect). |
| Instead, the process by which the compiler infers that the type argument |
| for `E` is the element type of the slice is called _constraint type |
| inference_. |
| |
| Constraint type inference deduces type arguments from type parameter |
| constraints. |
| It is used when one type parameter has a constraint defined in terms |
| of another type parameter. |
| When the type argument of one of those type parameters is known, |
| the constraint is used to infer the type argument of the other. |
| |
| The usual case where this applies is when one constraint uses the form |
| `~`_`type`_ for some type, where that type is written using other type |
| parameters. |
| We see this in the `Scale` example. |
| `S` is `~[]E`, which is `~` followed by a type `[]E` written in terms |
| of another type parameter. |
| If we know the type argument for `S` we can infer the type argument |
| for `E`. |
| `S` is a slice type, and `E` is the element type of that slice. |
| |
| This was just an introduction to constraint type inference. |
| For full details see the [proposal |
| document](https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md) |
| or the [language spec](/ref/spec). |
| |
| ### Type inference in practice |
| |
| The exact details of how type inference works are complicated, but |
| using it is not: type inference either succeeds or fails. |
| If it succeeds, type arguments can be omitted, and calling generic |
| functions looks no different than calling ordinary functions. |
| If type inference fails, the compiler will give an error message, and |
| in those cases we can just provide the necessary type arguments. |
| |
| In adding type inference to the language we've tried to strike a |
| balance between inference power and complexity. |
| We want to ensure that when the compiler infers types, those types are |
| never surprising. |
| We've tried to be careful to err on the side of failing to infer a |
| type rather than on the side of inferring the wrong type. |
| We probably have not gotten it entirely right, and we may continue to |
| refine it in future releases. |
| The effect will be that more programs can be written without explicit |
| type arguments. |
| Programs that don't need type arguments today won't need them tomorrow |
| either. |
| |
| ## Conclusion |
| |
| Generics are a big new language feature in 1.18. |
| These new language changes required a large amount of new |
| code that has not had significant testing in production settings. |
| That will only happen as more people write and use generic code. |
| We believe that this feature is well implemented and high quality. |
| However, unlike most aspects of Go, we can't back up that belief with |
| real world experience. |
| Therefore, while we encourage the use of generics where it makes |
| sense, please use appropriate caution when deploying generic code in |
| production. |
| |
| That caution aside, we're excited to have generics available, and we |
| hope that they will make Go programmers more productive. |