What We’ve Been Waiting For: Generics in Go
Language
- unknown
by James Smith (Golang Project Structure Admin)
It’s been a long time coming and it’s caused a certain degree of controversy among Gophers, but now we know, at last, that it’s really happening.
Generics have been included in a beta version of Go 1.18, which was just released this week.
Table of Contents
What Are Generics?
In a strictly typed language like Go, every argument that a function takes is of a specific type. This is the case even if the type is an empty interface (which also has the alias any
), which can hold a value of any underlying type.
However, this means that we often have to rewrite code just to handle different types. For example, if you wanted to create a function that adds up all the numbers in a slice and returns the final sum, you would — until recently — have had to write separate versions for integer slices and floating-point slices, even though the body of the function would be absolutely identical:
package main
import "fmt"
func sumInt(values []int) int {
var result int
for _, s := range values {
result += s
}
return result
}
func sumFloat64(values []float64) float64 {
var result float64
for _, s := range values {
result += s
}
return result
}
func main() {
fmt.Println(sumInt([]int{5, 3, 2})) // 10
fmt.Println(sumFloat64([]float64{5.2, 0.45, 9.875})) // 15.525
}
That seems like a wasteful way to write code, since we’re having to repeat ourselves just for the sake of being able to handle two slightly different types.
Later in this blog post, I will show you how to write a sum function that can handle slices with values of any numeric type, meaning that you achieve the same goal with much less typing.
Before we see how to use generics in Golang, however, I’m going to provide a quick run-through exploring how some other programming languages handle similar issues. We will look at Java, Typescript and Rust.
Generics in Java
Many programming languages use angle brackets — also known as the less than and greater than symbols — to pass type arguments to generic functions or classes. The LinkedList
class in Java (which implements the List
interface) is defined generically, meaning that it can accept different types depending on how it’s used.
import java.util.List;
import java.util.LinkedList;
import java.util.Collections;
public class Example {
public static void main(String []args){
List<Integer> numbers = new LinkedList<>();
numbers.add(-3);
numbers.add(5);
numbers.add(0);
numbers.add(99);
numbers.sort(Collections.reverseOrder());
System.out.println(numbers.toString());
}
}
We declare the numbers
variable, in the code above, to be a List
that holds values of the type Integer
. When we instantiate the LinkedList
class, we also use empty angle brackets, which signifies to the compiler that we want the type to be the same as for the List interface.
We then add some numbers to the list, sort them in reverse order and print them out on the screen.
Importantly, you can see below how easy it is to change the type argument, yet keep the rest of the functionality just the same as before. We can store and sort characters in exactly the same way as we did with integers:
import java.util.List;
import java.util.LinkedList;
import java.util.Collections;
public class Example {
public static void main(String []args){
List<Character> chars = new LinkedList<>();
chars.add('a');
chars.add('x');
chars.add('t');
chars.add('v');
chars.sort(Collections.reverseOrder());
System.out.println(chars.toString());
}
}
We sorted the characters in reverse alphabetical order, just as we were able to sort the numbers in descending order.
Generics in Typescript
Typescript — the strictly typed version of Javascript — also uses angle brackets to signify type arguments and parameters:
function stringify<Type>(arg : Type) : string {
return (
(typeof arg === "string")
? arg
: JSON.stringify(arg)
);
}
console.log(stringify(99));
console.log(stringify({data: 99}));
console.log(stringify("x"));
console.log(stringify(true));
In the example above, we declare a function that can accept an argument of any type. If the argument is a string, it simply returns it, otherwise it calls the JSON.stringify
function and returns its output.
This allows us to print out any variable by converting it to a string, if necessary.
Generics in Rust
Rust has had generic functions since its very first version was released in 2010. The language gradually built up a cult following and is now widely used in industry.
The example below shows a simple function that takes an array of arbitrary type. You can see that Rust likewise places its type parameters between angle brackets. We iterate through each element of the array and return the one whose value is smallest.
fn smallest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut result = list[0];
for &item in list {
if item < result {
result = item;
}
}
result
}
However, this code is slightly more complicated than our other examples, because we make use of Rust traits, which are like interfaces in other languages, to specify which specific types will be allowed: we use the PartialOrd
trait, which is necessary since we are comparing elements, and we use the Copy
trait, which is necessary since we want to store the elements in the result
variable.
How to Install Go 1.18
Before you’re able to write Golang code that takes advantage of the new generic syntax, you’ll need to install the beta version of Go 1.18 on your machine. You can do that by running the following command:
go install golang.org/dl/go1.18beta1@latest
Then you will need to perform updates:
go1.18beta1 download
You will now use the beta version to run any generic-based code that you write. So instead of calling go run
to execute your code, you will type:
go1.18beta1 run
If you try compiling generic functions with your default version of Go, you will get error messages that look something like the ones below, since it doesn’t understand the new syntax:
.\main.go:5:6: missing function body
.\main.go:5:9: syntax error: unexpected [, expecting (
.\main.go:8:2: syntax error: non-declaration statement outside function body
So make sure you only use the new executable for the beta version of Go 1.18 to compile your code. If you’re unsure, you can always run the go version
command, which will give you information about your current version of Go, to confirm that you’re using the correct one:
go version go1.18beta1 windows/amd64
Spend Some Time in the Gotip Playground
If you don’t want to install the beta version of Go 1.18 on your computer — perhaps because you want to wait until there’s a stable release — then don’t worry, because you can still take advantage of generics right now by coding online.
You can use the Gotip Playground, which always has the most recent version of Go from the development tree enabled. Hence the name: it only uses the top — or tip — of the tree, unlike the regular Go Playground which relies on more stable versions of Go (meaning that generics are not yet available there).
As I write this post, the regular Go Playground is currently using Go version 1.17.5. However, if you’re reading this at a later date, you can find out the version that’s being used simply by calling runtime.Version()
within the main
function.
Simply by visiting this website, you can write code that makes use of generic functions and run it entirely from your browser, wherever you are in the world. It doesn’t matter if you have Go installed or not: you can even use the site on your smartphone.
How to Use Generics in Go 1.18
I promised earlier that we would create a sum
function — using Go’s new generic syntax — that can handle any integer or floating-point number. Well, here it is:
package main
import "fmt"
func sum[T int|int8|int16|int32|int64|float32|float64](values []T) T {
var result T
for _, s := range values {
result += s
}
return result
}
func main() {
fmt.Println(sum([]int{5, 3, 2})) // 10
fmt.Println(sum([]float64{5.2, 0.45, 9.875})) // 15.525
fmt.Println(sum([]float32{0.64, 876.3, 1.333})) // 878.273
}
As we discussed in the previous section, this program won’t compile with older versions of Go, but it will compile perfectly well with the beta version of Go 1.18 that we’ve just installed.
When we declare the sum
function, you can see that there are square brackets between the function name and the opening brackets that contain the arguments: these are equivalent to the angle brackets that are used in the other programming languages we looked at. Within the square brackets, a custom type parameter is given a name, T
, followed by the underlying types that are allowed to be passed to the parameter.
In this case, we are using a union of all the signed integer and floating-point types, meaning that any one of them can be accepted by the function. The types within the union are separated by the vertical bar character.
So instead of using []int
or []float64
as the type of our argument, we can use []T
. Likewise, T
is our return type. We can even use T
within the function, as we do when declaring the result
variable.
You can see within the main
function that we can now call sum
with slices that hold any of the underlying types we specified in the type union, reducing our need to waste time by rewriting similar functions for different data types.
Using Generic Constraints in Go
Just as we saw with traits in Rust, it’s sometimes more useful not to give a union of underlying types, but rather to describe a condition that the underlying type must fulfil. This is what constraints do in the new version of Go.
package main
import (
"fmt"
"constraints"
)
func max[T constraints.Ordered](x, y T) T {
if x > y {
return x
}
return y
}
func main() {
fmt.Println(max(99, 11)) // 99
}
In the example above, we use the Ordered
interface from the new "constraints"
package, which tells the compiler that the arguments passed to the function must be of a type that is comparable using the >
, <
, >=
and <=
operators.
For example, we could pass two string
s or two uint64
s or even two uintptr
s, because variables of these types can all be directly compared with each other. They have an innate ordering — whereas, say, variables that are of a bool
or chan
type would not.
The "constraints"
package has not been fully documented yet, but it’s worth knowing about the other interfaces within the package that can be used when creating generic functions. They are listed in the table below:
Constraint | Description |
---|---|
Stringer | Can be converted to a string by calling the String method. |
Integer | Can be a signed or unsigned integer. |
Signed | Can be an signed integer. |
Unsigned | Can be an unsigned integer. |
Float | Can be a floating-point number. |
Complex | Can be a complex number. |
Slice | Can be a slice, holding elements of any type. |
Map | Can be a map, holding entries with keys and values of any type. |
Chan | Can be a channel, holding elements of any type. |
There is also the keyword comparable
, which is not a member of the "constraints"
package, but is a built-in part of the language: it describes a type that can be directly compared for equality using the ==
or !=
operators.
Final Word on the Inclusion of Generics in Golang
The recent introduction of generics is clearly one of the biggest new features to have been added to Go since its creation. The programming language has, over the years, evolved slowly and conservatively, putting a premium on stability and simplicity, rather than comprehensiveness.
Since we have not yet reached Go version 2.0, however, we can be confident that this recent update is intended to be entirely backward-compatible. It is widely assumed that the second major version of Go will only be released when significant and breaking changes are made to the language specification.