Back

Learning Generics in Go

Generics is released in Go 1.18 and it is time to learn how to leverage this new feature

by Percy Bolmér, January 31, 2022

A visual explanation of Generics, a line represented by a function, and the generic one accepts multiple inputs. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
A visual explanation of Generics, a line represented by a function, and the generic one accepts multiple inputs. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

Generics came to Go in update 1.18 (To be released in February 2022) with a bunch of other shiny new features. You can read about all the changes in my 1.18 patch note summary.

Go 1.18 is due to be released in February and it contains multiple changes that will improve the language
Go 1.18 Comes With Many Amazing Changes

January 31, 2022

Go 1.18 is due to be released in February and it contains multiple changes that will improve the language

Read more

To get started with 1.18 you can run, or download it here.

go install golang.org/dl/go1.18beta1@latest

While there are other nice things added, there is no doubt that the implementation of generics has overshadowed everything else. It is a topic that has been discussed for a very long time and there are many developers for it, and many against it.

Many people find generics tricky and complex, but let’s uncover the mystery. They are not that hard to use once you get familiar with them.

In this article, we will look at what generics are and learn how to use them.

My Video which covers Generics if you like it more than text

What Are Generics And Why Does Go Need Them

Generic function, allowing multiple types of input and output. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Generic function, allowing multiple types of input and output. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

Generics is a way of letting a function accept multiple data types as the same input parameter. Imagine that you have a function that should accept an input parameter, and subtract the value of the second input parameter.

You would need to decide upon a certain data type to use, an int, int64, or floats. This would force any developer using the subtract function to typecast their values into the correct data type before using it.

Another solution would be to have a subtract function for int, another function for floats, etc, leaving you with multiple functions doing the same work.

func main(){
	var a int = 20
	var b int = 10

	var c float32 = 20
	var d float32 = 10.5

	result := Subtract(a, b)
	// We need to cast data type into int here
	resultfloat := Subtract(int(c), int(d))

	// Will return 10
	fmt.Println("Result:", result)
	// Will return 10 -- Which is wrong, we should get 9.5 but we need a extra function for that to work
	fmt.Println("Resultfloat:", resultfloat)
}
// Subtract will subtract the second value from the first
func Subtract(a, b int) int {
	return a - b
}
Subtraction using the same Subtract function, by casting floats to ints

What if that typecasting or duplicate functions could be skipped? In the example above, we are getting the wrong result from the float result, as we remove the 0.5 when we convert it into an int.

So, the only proper solution would be having duplicate functions, one Subtract(a,b int) int and replica Subtractfloat(a,b float32) float32.

I say the duplicate function is the only solution, I know you can use the interface{} input and output solution. I don’t like that hack, it is error-prone and clunky. You also lose compile-time error checks since the compiler won’t know what you are doing with that interface. This quickly becomes unmaintainable and adds a lot of extra code.

That is the issue that generics aim to solve, and why so many developers have been very eager to see it released.

The first draft of generics is released now with Go 1.18 and the solution to the above problem is simple.

func main(){
	var a int = 20
	var b int = 10

	var c float32 = 20
	var d float32 = 10.5

	result := Subtract(a, b)
	// Here we tell the function that the input is a float32 data type, so expect the output parameter to be float32
	resultfloat := Subtract[float32](c, d)
	// Will output 10
	fmt.Println("Result:", result)
	// Will output 9.5
	fmt.Println("Resultfloat:", resultfloat)
}

// Subtract will subtract the second value from the first
func Subtract[V int64 | int | float32](a, b V) V {
	return a - b
}
Generic subtraction — a simple solution

Generics And How They Work — The Basics of Generic Functions

Beginning to learn generics by providing Type Parameters to a first function. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Beginning to learn generics by providing Type Parameters to a first function. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

Let’s dive in and learn the basic usage of generics. We will begin with the regular Subtract function and add generic features to it while we learn what it is that we add, and why.

The syntax for functions is that we define the function by stating func FunctionName which is followed by the function parameters.

Function parameters are defined inside () and can be as many as you like. A function parameter is defined by declaring a name, followed by the data type. For instance (a int) defines that the functions local scope will have an integer named a.

The Subtract method has the function parameters a and b of data type int.

func Subtract(a, b int) int {
  return a - b
}
Regular subtract function that we use as a base for learning generics.

Now, function parameters might seem trivial, but it is vital to understand before traversing into generics.

Aside from function parameters, there are also Type parameters. Type parameters are defined inside [] and should be defined before the function parameters, [](a,b int). You can define a type parameter the same way you would a function parameter, the name followed by the data type.

Type parameters are usually capitalized to make them easier to spot.

An example, where we declare that the parameter V is an integer, [V int]

// We define the Type parameter V, which is a int
func Subtract[V int](a, b int) int {
  return a - b
}
Type Parameter V has been added, this code does not yet run.

The difference between a function parameter and a type parameter is that the function parameter is available in the local scope of the function. If you define V the way we did above, you won’t be able to use V as a variable in the function. A type parameter only states what data types V represents.

What we define with the type parameter, is that there is a data type called V, which is an int. This allows us to use V as a replacement for int in the function parameter, and inside the scope of the function.

We can now replace the data type for a and b with V, and also the function output to V

// We define the Type parameter V, which is a int
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V int](a, b V) V {
  return a - b
}
Subtract now uses the type parameter V.

Now, you might think that we have accomplished nothing, we simply replaced int with a more complex solution. And you are right, we are not done, the above code won’t compile. You are not allowed to replace a single data type in a type parameter as we did unless putting it in an interface.

If you try to compile the current Subtract function you should see an error stating that int is not an interface.

The reason for this is that a type parameter expects a type constraint as the value, not a data type. A constraint is an interface that the function parameters have to fulfill.

We will add a second data type to the type parameter by using a | character. The pipe character is used to say or, meaning that we can add many different data type options to the V parameter. Using the | is also a shorthand way of creating a new inline interface.

We will add int32 and float32 as possible data types to V. Doing this will automatically create an interface that is used as a type constraint by the compiler.

// We define the Type parameter V, which is a int, int32 or float32
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V int | int32 | float32 ](a, b V) V {
	return a - b
  }
Type Parameters using | which creates a type constraint (interface) for us

You can try using the new Subtract method with a simple main function.

package main

import (
	"fmt"
)

func main(){
	var a int = 20
	var b int = 10
  // the compiler will infere the type used
	result := Subtract(a, b)

	fmt.Println("Result: ", result)
}

// We define the Type parameter V, which is a int, int32 or float32
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V int | int32 | float32 ](a, b V) V {
	return a - b
}
Using your first generic function

To make things more clear, we can break the type constraint out. Just to make it a bit easier to understand, and also, allow the constraint to be reusable.

To create the constraint we simply declare an interface Subtractable with the data types to include. It is the same syntax as the shorthand definition we used in the type parameter, so we can copy that out, I also added a bunch more data types.

package main

import (
	"fmt"
)
// Subtractable is a type constraint that defines subtractable datatypes to be used in generic functions
type Subtractable interface {
	int | int32 | int64 | float32 | float64 | uint | uint32 | uint64
}

func main(){
	var a int = 20
	var b int = 10

	result := Subtract(a, b)

	var c float32 = 20.5
	var d float32 = 10.3

	result2 := Subtract(c, d)
	
	fmt.Println("Result: ", result)
	fmt.Println("Result2: ", result2)
}

// We define the Type parameter V, which is has a Constraint of Subtratacble
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V Subtractable](a, b V) V {
	return a - b
}
Adding the Subtractable type constraint to the generic function

One last thing before you are a master of generics. You can apply a type parameter when you call the generic function to set the data type to use, this will make more sense when we get to more advanced usages.

The way to add that is to again use the type parameter, but this time in front of the function call.

result := Subtract[int](a, b)

Type Arguments and Tilde (~)

Type Arguments are provided to function call to specify data type. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Type Arguments are provided to function call to specify data type. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

So we can use generic functions by now, but there are still some details left. Imagine if you want to control the data type to use in a generic function?

Say you want to use the Subtract function that we created, it will infer the data types, unless explicitly told. You can do this by Type Arguments and the syntax is the same as when defining the generic function. You provide the data type in brackets before the parameters.

Subtract[int](10,20) // Will be using int datatype 
Subtract[int64](10,20) // Will be using int64 datatype
package main

import (
	"fmt"
	"reflect"
)

// Subtractable is a type constraint that defines subtractable datatypes to be used in generic functions
type Subtractable interface {
	int | int32 | int64 | float32 | float64 | uint | uint32 | uint64
}

// We define the Type parameter V, which is has a Constraint of Subtratacble
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V Subtractable](a, b V) V {
	return a - b
}

func main() {
	result := Subtract(10,20)
	// will Print int
	fmt.Println(reflect.TypeOf(result))
	
	result2 := Subtract[float32](10,20)
	// will Print Float32
	fmt.Println(reflect.TypeOf(result2))
}
main.go — Using Type Arguments upon calling the function to decide on data type to use

Horray! We can now even tell the function what data type to use, excellent!

Now, there is a problem here still. What if you have a data typed that is an Alias/derived to any of the types inside Subtractable?

You won’t be able to use your data types with the way we currently declared subtractable.

// create a custom int derived from int
type MyOwnInteger int
var myInt MyOwnInteger
myInt = 10
Subtract(myInt, 20) // This will Crash, Since myInt is not Subtractable

To solve this issue, the Go team also added the ~ which tells the constraint that any type that is derived from the given type is allowed. In the example below we allow MyOwnInteger to be part of Subtractable by applying the ~ in front of it.

package main

import (
	"fmt"
	"reflect"
)

// Subtractable is a type constraint that defines subtractable datatypes to be used in generic functions
type Subtractable interface {
	~ int | int32 | int64 | float32 | float64 | uint | uint32 | uint64
}

type MyOwnInteger int

// We define the Type parameter V, which is has a Constraint of Subtratacble
// We also say that function parameter a and b has the data type of V
// We then make the function return V
func Subtract[V Subtractable](a, b V) V {
	return a - b
}

func main() {
	
	var myint MyOwnInteger
	myint = 10
	
	result := Subtract(myint, 20)
	// will Print main.MyOwnInteger
	fmt.Println(reflect.TypeOf(result))
	
	result2 := Subtract[float32](10, 20)
	// will Print Float32
	fmt.Println(reflect.TypeOf(result2))
}
Using the ~ to allow alias types to be part of the type constraint

Generic Types, Any & Comparable

New Aliases introduced in Go to be used as type constraints, any and comparable. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
New Aliases introduced in Go to be used as type constraints, any and comparable. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

We have covered generic functions and it is time to cover generic types.

To create a generic type you have to define a new type, and give it a type parameter. To learn this we will create a Results type, which is a slice of any data type found in the previously created Subtractable constraint.

// Results is a array of results, reusing the type constraint Subtractable
type Results[T Subtractable] []T
Generic Type Results which is a slice of Subtractable data types.

The syntax should not be new, and I hope you by now can understand what is going on in the generic type definition. It is the same as in the previous examples. In the example we create a slice where the data entries will be of the data type Subtractable, thus telling the compiler that when a slice of Results is initialized, a type included in the Subtractable interface has to be defined.

To use Results we will update the main function, and notice how we have to define the data type that the Results slice is going to use when creating the variable.

func main(){
	var a int = 20
	var b int = 10

	result := Subtract(a, b)

	var c float32 = 20.5
	var d float32 = 10.3

	result2 := Subtract[float32](c, d)
	result3 := Subtract(c, d)
	
	fmt.Println("Result: ", result)
	fmt.Println("Result2: ", result2)
	fmt.Println("Result3: ", result3)
	// Create a generic Results type, and set the instantitation to int
	var resultStorage Results[int]

	resultStorage = append(resultStorage, result)

	fmt.Println("ResultStorage: ", resultStorage)
}
Using the Results generic type (A slice of generic data types)

Now, I already know what you are thinking, Can we say that Results should use the Subtractable type constraint?

var resultStorage Results[Subtractable]

Sadly, Subtractable is a type constraint, and we cant use that in the initialization of the Results. Doing so will trigger a compile error interface contains type constraints.

What we can do is use the newly introduced any to allow the Results to hold any data type. any is an alias for interface{}.

// Results is a array of results, the data types can be any
type Results[T any] []T

// Subtractable is a type constraint that defines subtractable datatypes to be used in generic functions
type Subtractable interface {
	int | int32 | int64 | float32 | float64 | uint | uint32 | uint64
}

func main(){
	var a int = 20
	var b int = 10

	result := Subtract(a, b)

	var c float32 = 20.5
	var d float32 = 10.3

	result2 := Subtract[float32](c, d)
	result3 := Subtract(c, d)
	
	fmt.Println("Result: ", result)
	fmt.Println("Result2: ", result2)
	fmt.Println("Result3: ", result3)
	// Create a generic Results type, and set the instantitation to int
	var resultStorage Results[any]
	// We can now append all values
	resultStorage = append(resultStorage, result, result2, result3)

	fmt.Println("ResultStorage: ", resultStorage)
}
ResultStorage can now hold all the values from the Subtract.

It is not perfect yet, it is the first draft. I hope we see a way of adding the Subtractable in the future, to avoid needing to use any since it allows us to add all kinds of data types.

There is another new type named comparable which is a type constraint that is fulfilled by any data type that can be compared by using == or !=.

It is important to get familiar with these words as you will probably see them appearing all over the as generics become more familiar to the community.

Interface Constraints & Generic Structs

Interfaces can be used as type constraints. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)
Interfaces can be used as type constraints. Image by Percy Bolmér. Gopher by Takuya Ueda, Original Go Gopher by Renée French (CC BY 3.0)

So far we have only used single constraints and outputs. We have only used type constraints however it is possible to use interfaces as constraints as well.

Let us try making a generic function that accepts an interface called Moveable. The function will simply trigger the Move for the input types, any struct fulfilling this interface should be able to Move.

// Move is a generic function that takes in a Moveable and moves it
func Move[V Moveable](v V, meters int) {
	v.Move(meters)
}
Move — generic function that has a constraint that input v has to be Moveable

You should be familiar with the syntax, we create a constraint saying that the type V has to be Moveable and that the input parameter v is the type V.

We will also create a Person and a Car to try it out.

// Moveable is a interface that is used to handle many objects that are moveable
type Moveable interface {
	Move(int)
}
// Person is a person, implements Moveable
type Person struct {
	Name string
}

func (p Person) Move(meters int) {
	fmt.Printf("%s moved %d meters\n", p.Name, meters)
}
// Car is a test struct for cars, implements Moveable
type Car struct {
	Name string
}

func (c Car) Move(meters int) {
	fmt.Printf("%s moved %d meters\n", c.Name, meters)
}
// Move is a generic function that takes in a Moveable and moves it
func Move[V Moveable](v V, meters int) {
	v.Move(meters)
}

func main(){
	p := Person{Name: "John"}
	c := Car{Name: "Ferrari"}
	// Since the V paramter accepts Moveable, we can now call Move on Both Structs
	Move(p, 10)
	Move(c, 20)
}
Generic way of moving multiple structs.

So far, we have only been using one generic parameter to keep it simple. But remember that you can add multiple, and output multiple.

Let’s combine the Moveable and Subtractable constraints to the Move function, allowing users to add a Distance value which we use to calculate how far it is until the goal.

To add more type constraints simply add parameters inside the [] just as with regular parameters. We will add the Subtractable type defined as S and instead of accepting meters as an Int, we will allow it to be S

This is how our Move function would look with both Type Constraints

func Move[V Moveable, S Subtractable](v V, distance S, meters S) S

However, this change alone would cause the compiler to cry because the Move function accepts an Int and the Moveable defined that this is the way Move should work. So we need to make the Move accept Subtractable.

// Moveable is a interface that is used to handle many objects that are moveable
type Moveable interface {
	Move(Subtractable)
}

// Person is a person, implements Moveable
type Person struct {
	Name string
}

func (p Person) Move(meters Subtractable) {
	fmt.Printf("%s moved %d meters\n", p.Name, meters)
}
// Car is a test struct for cars, implements Moveable
type Car struct {
	Name string
}

func (c Car) Move(meters Subtractable) {
	fmt.Printf("%s moved %d meters\n", c.Name, meters)
}
// Move is a generic function that takes in a Moveable and moves it
func Move[V Moveable, S Subtractable](v V, distance S, meters S) S {
	v.Move(meters)
	return Subtract(distance, meters)
}
Adding Subtractable to the Move function and changing Int to Subtractable

This looks amazing right?! Sadly, the gist above is not a working example, this is just pseudo-code for what we want to accomplish. The code above will not compile, the compiler will yell at you angrily because we are using a Type Constraint inside an Interface which is not allowed, remember?

There is a way to get what we want though, Generic Structs. Generic structs are structs that get their data type defined during initialization.

Special thanks to u/ar1819 for helping me find this solution

We need to define the S type for the interface the same way we do for the generic functions. By simply adding [S Subtractable] to the interface declaration we say that not only does the Struct need the same set of methods to be part of the interface, but it also needs to be a Generic Struct.

// Moveable is a interface that is used to handle many objects that are moveable
// To implement this interface you need a Generic Type with a Constraint of Subtractable
type Moveable[S Subtractable] interface {
	Move(S)
}
Adding Type constraint to Interface

If that is the rule for the contract, let us add that to the Car and the Person as well. Those structs will now be Generic. What this means is that when creating an object, you have to also define the data type to use for that object.

// Car is a Generic Struct with the type S to be defined
type Car[S Subtractable] struct {
	Name string
}

// Person is a Generic Struct with the type S to be defined
type Person[S Subtractable] struct {
	Name string
}
This is how to declare generic structs in Go

To create our Car and Person, we need to specify what data types they use. This is done as all generic features, with []. Remember, they are called Type Arguments.

func main(){
	// John is travelling to his Job
	// His car travel is counted in int
	// And his walking in Float32
	p := Person[float64]{Name: "John"}
	c := Car[int]{Name: "Ferrari"}

}
Initializing two generic structs, assigning the data type by using []

Note that we are now working with Person[S Subtractable] and not only Person, so all methods also need to use this initialization.

// Person is a struct that accepts a type definition at initialization
// And uses that Type as the data type for meters as input
func (p Person[S]) Move(meters S) {
	fmt.Printf("%s moved %d meters\n", p.Name, meters)
}
func (c Car[S]) Move(meters S) {
	fmt.Printf("%s moved %d meters\n", c.Name, meters)
}
Making generic struct part of the Moveable interface requires [S] set

We should also make the Move function now accept a Generic Movable by upgrading into, and also accept the Subtractable constraint.

// Move is a generic function that takes in a Generic Moveable and moves it
func Move[V Moveable[S], S Subtractable](v V, distance S, meters S) S {
	v.Move(meters)
	return Subtract(distance, meters)
}
Moveable has to have its type defined [S]

We are ready to create the main function that uses all the things we have done. The first Move call using the Car is easy to understand, this is because we use int and the compiler will default to that.

However, the second call is more complex, since we now want to use the float64 data type. To do this we need to prepend the Move() call with the regular [] in where we define the Type Arguments. In this case, the Moveable will be a Person initialized as float64. And the datatype for Subtractable will also be a float64. So the type definition will be [Person[float64], float64]Move().

func main(){
	// John is travelling to his Job
	// His car travel is counted in int
	// And his walking in Float32
	p := Person[float64]{Name: "John"}
	c := Car[int]{Name: "Ferrari"}
	
	
	// John has 100 miles to his job
	milesToDestination := 100
	// John moves with the Car
	distanceLeft := Move(c, milesToDestination, 95)
	// John has 5 miles left to walk after parking (phew)
	fmt.Println(distanceLeft)
	fmt.Println("DistanceType: ", reflect.TypeOf(distanceLeft))

	// Jumps out of Car and Walks to Building
	// Again we need to define the data type to use for the Move function, or else it will default to int
	// So here we have to tell Move to initialize with a Person type, with a float64 value,
	// And that the Subtract data type is also float64
	// [Move[float64], float64]
	// distanceLeft is also a INT, since Move defaulted to int in previous call, so we need to convert it
	newDistanceLeft := Move[Person[float64], float64](p, float64(distanceLeft), 5)
	fmt.Println(newDistanceLeft)
	fmt.Println("DistanceType: ", reflect.TypeOf(newDistanceLeft))

}
Calling the Move function for both Car and Person

A tip is to think about the order you place the type arguments in. We can avoid the Move[Person[float64], float64] by reordering the type arguments in Move. This will work thanks to the compiler and runtime inferring datatypes.

// Move is a generic function that takes in a Moveable and moves it
// Subtractable is placed infront of Moveable instead
func Move[S Subtractable, V Moveable[S]](v V, distance S, meters S) S {
	v.Move(meters)
	return Subtract(distance, meters)
}
// You can now do this instead of having to define Person 
newDistanceLeft := Move[float64](p, float64(distanceLeft), 5)
Ordering of Type arguments can be syntactically nicer

Conclusion

Congratulations, if you made it here you are now an expert in the subject of Generics!

I hope you will find them useful, I know many people have been waiting much for this release. Many create their libraries for Sorting slices, maps using Queues, etc. There will probably be a lot of new packages and new APIs for old libraries shortly.

Many use cases are great with generics, but sometimes it is easy to make things a bit too complex instead. Try only using them when there is an actual use case for them.

If you like my writing, don’t miss my guide for the fuzzing feature in Go, which was also released in Go 1.18!

Fuzzing is a technique where you automagically generate input values for your functions to find bugs
Fuzzing Tests in Go

January 31, 2022

Fuzzing is a technique where you automagically generate input values for your functions to find bugs

Read more

Feel free to reach out on any of my social media below

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter