Changes

The draft is a living document which means these posts will need to change over time. This section documents when changes have taken place to this post.

21/08/20 : Moving forward with the generics design draft

Series Index

Generics Part 01: Basic Syntax
Generics Part 02: Underlying Types
Generics Part 03: Struct Types and Data Semantics

Introduction

In this series of posts about generics in Go, I will present code and teach to the different aspects of the current generics draft. I will provide code links with the go2go playground so you can experiment with the different examples. The code samples will also be available in the Go training repo, but these examples are subject to change as I learn more or the draft changes.

In this first post, I will share a basic example of a generic function, break down the new syntax, and explain why a new syntax is needed. The code for this post can be found at this playground link.

Concrete Example

What if you wanted to write a print function that can output a slice of integers?

Listing 1

13 func printNumbers(numbers []int) {
14     fmt.Print("Numbers: ")
15     for _, num := range numbers {
16         fmt.Print(num, " ")
17     }
18     fmt.Print("\n")
19 }

Main:
numbers := []int{1, 2, 3}
printNumbers(numbers)

Output:
Numbers: 1 2 3

Listing 1 shows an implementation of a print function that can output a slice of integers using 5 lines of code. I will argue that the majority of Go developers at all experience levels can quickly read and maintain this code with little effort.

What if you wanted to write a print function that can output a slice of strings?

Listing 2

21 func printStrings(strings []string) {
22     fmt.Print("Strings: ")
23     for _, str := range strings {
24         fmt.Print(str, " ")
25     }
26     fmt.Print("\n")
27 }

Main:
strings := []string{"A", "B", "C"}
printStrings(strings)

Output:
Strings: A B C

Listing 2 shows an implementation of a print function that can output a slice of strings using essentially the same 5 lines of code as the integer version. The only difference between these two functions is that the printStrings function accepts a slice of strings (not integers) and on line 22, the print statement displays the word Strings instead of Numbers. Since I want to accept a slice of strings, I need a new function.

Empty Interface and Type Assertions

What if you wanted to write a single print function that could output both a slice of integers and strings? One option is to use the empty interface and type assertions.

Listing 3

35 func printAssert(v interface{}) {
36     fmt.Print("Assert: ")
37     switch list := v.(type) {
38     case []int:
39         for _, num := range list {
40             fmt.Print(num, " ")
41         }
42     case []string:
43         for _, str := range list {
44             fmt.Print(str, " ")
45         }
46     }
47     fmt.Print("\n")
48 }

Main:
numbers := []int{1, 2, 3}
strings := []string{"A", "B", "C"}
printAssert(numbers)
printAssert(strings)

Output:
Assert: 1 2 3
Assert: A B C

Listing 3 shows an implementation of a single print function that can output both a slice of integers and strings using the empty interface and type assertions. This function can accept a slice of integers, or a slice of strings (or a value of any type) since the empty interface does not put a constraint on the data being passed in. On line 37, a type assertion is used in the switch statement to apply conditional logic and test whether a slice of integers or strings was passed into the function. Each case statement provides the logic depending on the result of the type assertion.

Because each slice of a given type needs to be implemented via a case statement, this function is really not a generic function. It’s currently limited to only printing slices of integers and strings. All this function has done is essentially replaced the two concrete functions for case statements. If you wanted to print a slice of float64, an additional case statement for a slice of that type needs to be coded.

Reflection

What if you wanted to write a single print function that could output a slice of any given type? The option you have today is to use the reflect package.

Listing 4

56 func printReflect(v interface{}) {
57     fmt.Print("Reflect: ")
58     val := reflect.ValueOf(v)
59     if val.Kind() != reflect.Slice {
60         return
61     }
62     for i := 0; i < val.Len(); i++ {
63         fmt.Print(val.Index(i).Interface(), " ")
64     }
65     fmt.Print("\n")
66 }

Main:
numbers := []int{1, 2, 3}
strings := []string{"A", "B", "C"}
floats := []float64{1.5, 2.9, 3.1}
printReflect(numbers)
printReflect(strings)
printReflect(floats)

Output:
Reflect: 1 2 3
Reflect: A B C
Reflect: 1.5 2.9 3.1

Listing 4 shows an implementation of a single print function that can output a slice of any type using the empty interface and the reflect package. This function once again can accept a slice of any concrete type since the empty interface does not create a constraint on the data being passed in. Thanks to the reflect package, I can write code to perform a linear traversal over the slice regardless of its type, as seen on lines 62 and 63.

However this function isn’t perfect. The return statement on line 60 should return an error value if the caller doesn’t pass a slice, which breaks the original API. The code is also not as simple and intuitive as the concrete version. Knowledge of the reflect package and its API is required. In the end, this is a generic function that can print the individual values of any slice, so I could argue Go has generics already.

A question I ask myself is this:

Would there be value in having a single print function that could work with a generic type such that the concrete implementation could be reused and the reflection code avoided?

I think there would be value since a function like this would be simpler and more intuitive to read and maintain than the reflection version while providing the same functionality.

Generics

What if you wanted to write a single print function that could output a slice of any given type and not use reflection as we did in the previous example? This is where the new generics support comes in.

Listing 5

75 func print[T any](slice []T) {   |   13 func printNumbers(numbers []int) {
76     fmt.Print("Generic: ")       |   14     fmt.Print("Numbers: ")
77     for _, v := range slice {    |   15     for _, num := range numbers {
78         fmt.Print(v, " ")        |   16         fmt.Print(num, " ")
79     }                            |   17     }
80     fmt.Print("\n")              |   18     fmt.Print("\n")
81 }                                |   19 }

On the left hand side of listing 5, you can see an implementation of a single print function that can output a slice of any given type using the new generics syntax being proposed by the current draft. I have also included on the right hand side, the concrete print function accepting a slice of integers for a side by side comparison of the code. If you look closely, both functions are essentially the same, minus the different variable names and the label for the initial print call on lines 76 and 14.

I’m back to the 5 lines of code I believe the majority of Go developers at all experience levels can quickly read and maintain.

Understanding The New Syntax

To understand the new syntax, it’s important to understand a part of the problem space for the compiler.

Listing 6

75 func print(slice []T) {
76     fmt.Print("Generic: ")
77     for _, v := range slice {
78         fmt.Print(v, " ")
79     }
80     fmt.Print("\n")
81 }

In listing 6, I have removed the square brackets that were between the function name and the parameter list from listing 5. With the square brackets removed, this function is telling the compiler that the function will accept a slice of type T that will be explicitly declared somewhere in the package. I want to reiterate, the compiler is expecting that a type named T will be declared somewhere in the package.

What happens if I try to compile this code?

Listing 7

undeclared name: T

Listing 7 shows the compiler message when the compiler doesn’t have a declaration for type T explicitly defined. Hence the problem. If you are going to write a generic function, you need a way to tell the compiler that you won’t be declaring (in this case) type T explicitly, but it has to be determined by the compiler at compile time.

The important piece here is the language needs a syntax choice to tell the compiler that type T is a type the programmer won’t be declaring prior to the program being compiled. That it’s up to the compiler to figure out what type T is at compile time.

Generic Type Lists

In the current draft, this is done is with a set of square brackets that define a list of generic type identifiers.

Listing 8

75 func printGeneric[T any](slice []T) {
76     fmt.Print("Generic: ")
77     for _, v := range slice {
78         fmt.Print(v, " ")
79     }
80     fmt.Print("\n")
81 }

Listing 8 shows the generic print function again with the set of square brackets put back in. These brackets are used to define a list of generic type identifiers that represent types specific to this function that need to be determined at compile time. It’s how you tell the compiler that types with these names won’t be declared before the program is compiled. These types need to be figured out at compile time.

Note: You can have multiple type identifiers defined inside the brackets though the current example is only using one. Ex. [T, S, R any]

You can name these type identifiers anything you feel will help with the readability of the function. In this case, I’m using the capital letter T to describe that a slice of some type T (to be determined at compile time) will be passed in. I like the use of single capitalized letters when it comes to collections and it’s also a convention that goes back to older programming languages like C++ and Java.

Use Of Any

New to the draft as of 21/08/20, a constraint must be provided to each type identifier.

To avoid the ambiguity with array declarations, we will require that all type parameters provide a constraint. This has the advantage of giving type parameter lists the exact same syntax as ordinary parameter lists (other than using square brackets). To simplify the common case of a type parameter that has no constraints, we will introduce a new predeclared identifier “any” as an alias for “interface{}”.

I will talk about constraints in a later post, but for now any declares that there are no contraints on what T can be at compiler time.

Calling Generic Functions

How would a user make a call to this generic print function?

Listing 9

numbers := []int{1, 2, 3}
print[int](numbers)

strings := []string{"A", "B", "C"}
print[string](strings)

floats := []float64{1.7, 2.2, 3.14}
print[float64](floats)

Listing 9 shows how to make calls to the generic print function where the type information for T is explicitly provided at the call site. The syntax emulates the idea that the function declaration func name[T any](slice []T) { defines two sets of parameters. The first set is the type that maps to the corresponding type identifiers, and the second is the data that maps to the corresponding input variables.

Luckily, the compiler can infer the type and eliminate the need to explicitly pass in the type information at the call site.

Listing 10

numbers := []int{1, 2, 3}
print(numbers)

strings := []string{"A", "B", "C"}
print(strings)

floats := []float64{1.7, 2.2, 3.14}
print(floats)

Listing 10 shows how you can call the generic print functions without the need to pass the type information explicitly. At the function call site, the compiler is able to identify the type to use for T and construct a concrete version of the function to support slices of that type. The compiler has the ability to infer the type with the information it has at the call site from the data being passed in.

Angle Brackets

A big point of discussion on the Go mailing list is why angle brackets are not being used like in C++ and Java. It’s not feasible to use angle brackets in Go and I will explain why.

Listing 12

func print<type T>(list []T) {

print<int>(numbers)
print<string>(strings)
print<float64>(floats)

Listing 12 shows what the generic print function and explicit function calls would look like if angle brackets were used. The big question is why is this not viable as an operator choice? It has to do with maintaining backwards compatibility with the current language spec.

Here is code that you can write today in Go.

Listing 13

w, x, y, z := 10, 20, 30, 40
a, b := w < x, y > (z)
fmt.Println(a, b)

Output:
true false

Listing 13 shows the declaration of two variables a and b being assigned values from the two expressions on the right hand side of the short variable declaration operator. The compiler breaks that line of code down into these two statements.

Listing 14

a := w < x
b := y > (z)

Listing 14 shows how a and b would be assigned the result of the boolean expressions. What happens if the angle brackets are the choice for declaring the generic type list? Given the expression back in listing 13, the compiler now has ambiguity.

Listing 15

a, b := w < x, y > (z)

// Is it this like before?
a := w < x
b := y > (z)

// Or is it this?
a, b := w<x, y>(z)

Listing 15 shows how it’s not obvious anymore if the angle brackets represent less than and greater than operators or if they represent a call to a generic function. Since type information is not available to the compiler at compile time, any of these identifiers may be declared in another source code file that has not been parsed yet. This ambiguity can’t be resolved without breaking the backwards compatibility promise of the language, so angle brackets are just not an option in Go.

Conclusion

After reading this post, you should have a better understanding of the basic syntax for generic functions in Go and the solution for declaring types to be determined at compile time versus those that will be explicitly defined. You should see the need for the square brackets to form a generic type list when declaring a generic function. With the compiler’s ability to infer the generic types at the function call site, you should see that calling a generic function is no different than calling any other function.

I showed you how writing generic functions using the new syntax can reduce the complexity of writing generic functions with the reflect package. I showed you how removing the use of the empty interface and reflection code in these examples helped to increase the readability and maintainability of the code.

In the next post, I will explore how generics can be used to define user defined types. If you can’t wait, I recommend you check out the repo of code that these blog posts are based on and experiment for yourself. If you have any questions, please reach out to me over email, Slack, or Twitter.

Trusted by Top Technology Companies

We've built our reputation as educators and bring that mentality to every project. When you partner with us, your team will learn best practices and grow along the way.

30,000+

Engineers Trained

1,000+

Companies Worldwide

14+

Years in Business