Professional Documents
Culture Documents
Generics - Practical Go Lessons-38
Generics - Practical Go Lessons-38
com/chap-38-generics
• We will see the traditional use cases of generics: when to use them, when not to use them
• Type constraints
• Type parameters
• Function
• Method
• Interface
2 Introduction
Since Go’s first release, the community’s need for generics has been strong. As mentioned by Ian Lance Taylor in a talk that a Go user
requested it in November 20091! The Go team introduced Generics 1.18 version that was released in March 20222
Here is an example usage that will make you understand the notion: Jazz is a generic term for a wide range of different styles of music..
If we come back to programming, we can create, for instance, generic functions that will not bind to a specific type of input/output
parameters.
When I build a function in Go, I need to specify the type I use for my input parameters and my results. Usually, a function will work for a
specific type, for instance, an int64 .
1 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
// generics/first/main.go
This max function will only work if you input numbers of type int64 :
// generics/first/main.go
var a, b int64 = 42, 23
fmt.Println(max(a, b))
// 42
But let’s imagine now that you have numbers of type int32, they are integers, but the type is not int64. If you attempt to feed the max
function with int32 numbers, your program will not compile. And this is very fine; int32 and int64 are not the same types.
// generics/first/main.go
var c, d int32 = 12, 376
// DOES NOT COMPILE
fmt.Println(max(c, d))
// ./main.go:10:18: cannot use c
// (variable of type int32) as type int64 in argument to max
// ./main.go:10:21: cannot use d
// (variable of type int32) as type int64 in argument to max
The idea behind generics is to make that function work for int, int32, int64 but also unsigned integers: uint, uint8, uint16, uint32. Those types
are different, but they all share something particular we can compare them the same way.
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
We now understand that generic programming aims to write interoperable and adaptable code. But what does it mean to have an
interoperable code?
It means that we want to be able to use the same function for different types that share some common capabilities.
Let’s come back to our previous example: the max function. We could write different versions of it for each integer type. One for uint, one for
uint8 , one for int32 , etc...
// generics/first/main.go
Writing the same function over and over will work, but it is ineffective; why not just one function that will work for all integers? Generics is the
language feature that allows that.
2 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
// generics/first/main.go
// generics/first/main.go
fmt.Println(maxGeneric[int64](a, b))
// 42
fmt.Println(maxGeneric[int32](c, d))
// 376
// generics/first/main.go
fmt.Println(maxGeneric(a, b))
fmt.Println(maxGeneric(c, d))
interface{}
All types implement the empty interface. It means that I can define a new max function that accepts as input elements of type empty
interface and returns elements of type empty interface :
// generics/first/main.go
Can I do that? No! The program will not compile. We will have the following error :
The empty interface does not define the operator greater than ( > ). And we touch here on an important particularity of types: one type can
define behaviors, and those behaviors are methods but also operators.
You might ask yourself what exactly an operator is. An operator combines operands. The most known are the ones you can use to compare
things :
When we write:
A > B
So we cannot use the empty interface because it says nothing, and by the way, with Go 1.18, the empty interface now has an alias: any.
Instead of using interface{} you can use any. It will be the same, but please remember that there is a good old empty interface behind
any.
3 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
We can create generic functions or methods by adding type parameters. We use square brackets to add type parameters to a regular
function. We say we have a generic function/method if we have type parameters.
// generics/first/main.go
package main
import (
"golang.org/x/exp/constraints"
)
In the previous snippet, the function maxGeneric is generic. It has one type parameter named T of type constraint constraints.Ordered. This
type comes from the package constraints that the Go team provides.
• Note that the identifier of the type parameter is positioned before the type constraint.
A generic function has a list of type parameters. Each type parameter has a type constraint, just as each ordinary parameter has a type3.
4 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
The type(s) constraint(s) will restrain the types we can use in our generic function. It gives you the info: can I use this specific type in this
generic function.
This definition is a bit hard, but in reality, it’s not that complex; let’s decompose it:
• It should be an interface
• In this interface, we define the set of permissible type arguments, all types we can use for this parameter.
After seeing the theory, let’s take a look at what a type constraint looks like in reality :
So we can see that inside this interface, we do not have methods. Like we have in a traditional interface. Instead of methods, we have one line
:
You have three elements separated by the pipe character: | . This character represents a union. We name type terms the elements that form
that union. Integer , Float and ~string are type terms.
• A single type
▪ Here, for instance, string is a predeclared type (It exists in the language by default, like int , uint8 , .…)
▪ And for instance, we can have Integer which is a type that we created.
• An underlying type
◦ The tilde denotes all the types that have a specific underlying type.
◦ Generally, we do not target a specific type like int even if we can do that; we will prefer to target all the other types that can
exist with the underlying type int . By doing so, we cover more types.
◦ For example: ~string denotes all the types that have the underlying type string :
This type DatabaseDSN is a new type, but the underlying type is string , so as a consequence, this type fits into ~string .
5 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
This type PrimaryKey is a new type, but the underlying type is uint , so as a consequence, this type fits into ~uint .
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
You will need first to import the package into your code with:
go get golang.org/x/exp/constraints
◦ ex: -10, 10
◦ ex: 42
◦ Signed | Unsigned
• Float all floating point numbers
◦ ~float32 | ~float64
• Complex all complex numbers
◦ ~complex64 | ~complex128
• Ordered all types that we can order
◦ As you note here, this is the union between three types integers, float, and strings
6 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
We have seen in the previous example that to call our maxGeneric we needed to specify the argument type:
It is clear here that the type of T is int64 since we manipulate int64 . Why is it important for the language to determine the type of T ,
the type of the type parameter? That’s because, in any part of our program, we need to have variables that have a type. When we define our
generic function, we add a type parameter that constrains the types we can use. When I define my maxGeneric function, I only know that I
can order the arguments passed to my function. It does not say much more.
When we want to use the function, Go needs to determine what will be concretely the type that we will manipulate. At runtime, the program
work on concrete, specific types, not a formal constraint, not a catalog of every type possible.
maxGeneric[int64](a, b)
In the last snippet, we did not specify the type parameter of a and b; we let the language infer the type parameter (the type of T ). Go will
attempt to determine the type parameter based on the context.
This type parameter inference is done at compile time and not runtime.
Note that type parameter inference might not be possible in some cases. We will not deep dive into those exceptions. Most of the time,
inference will work, and you will not have to think about it. In addition, there is a strong safeguard because the compiler checks inference.
// generics/types
type GenericMap[K constraints.Ordered, V constraints.Integer] map[K]V
We have a new type, GenericMap , with a parameter list composed of 2 parameter types: K and V . The first parameter type ( K ) has the
constraint ordered; the second parameter type has the type constraints.Integer .
This new type has an underlying type which is a map . Note that we can also create generic type structs (see figure).
7 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
Why create this new type? We already can create a map with a specific concrete type... The idea is to use that type to build function/methods
that can be used on a lot of different map types: map[string]int32 , map[string]uint8 , map[int]int ,... etc. For instance, summing all the
values in the map:
// generics/types
// generics/types
m := GenericMap[string, int]{
"foo": 42,
"bar": 44,
}
m2 := GenericMap[float32, uint8]{
12.5: 0,
2.2: 23,
}
// generics/types
fmt.Println(m.sum())
// 86
fmt.Println(m2.sum())
// 23
But let’s say now I want to create a new variable of type map[string]uint :
8 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
// generics/types
m3 := map[string]uint{
"foo": 10,
}
fmt.Println(m3.sum())
The answer is no; that’s because the sum is only defined on elements of type GenericMap . Fulfilling the constraints is insufficient; we will
need to convert it to a GenericMap . And it is done like that :
m4 := GenericMap[string, uint](m3)
fmt.Println(m4.sum())
We use parenthesis to convert m3 to a valid GenericMap . Please note that you will need to provide the parameter list and explicitly state
the types string and uint in this case.
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
12.1 First use case: when you write almost the same function several times
When you write a method/function several times, the only thing that changes is the input/output type. In that case, you can write a generic
function/method. You can replace a bunch of functions/methods with one generic construct!
// generics/use-cases/same-fct2
Here we have two functions that check if an element is in a slice. What is changing between those two functions? The type of the slice
element. This is a perfect use case to build a generic function!
// generics/use-cases/same-fct2
We have created a generic function named contains. This function has one type parameter of type constraints.Ordered . This means we can
9 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
compare the two elements of the slice because we can use the operator == with types that fulfill this constraint.
If we take the definition from Wikipedia: a data structure is a data organization, management, and storage format that is usually chosen for
efficient access to data. More precisely, a data structure is a collection of data values, the relationships among them, and the functions or
operations that can be applied to the data.
So a data structure is a way to store and organize data. And this data structure is also shipped with functions/operations that we can use on
it.
The most common data structure we have seen is the map. A map allows us to store some data in a particular way, to retrieve and eventually
delete it.
• The linked list: each element in this list points to the next one
◦ the context package is built around this data structure (see the chapter about context)
• The binary tree is a data structure that uses the graph theory, each element (called a node) has at most two children nodes.
Why does it make sense to have a generic linked list? It makes sense because the data structure does not depend on what type of data you
want to store. If you want to store integers in a linked list, the internals of the linked list will not differ from those operating on strings.
There is one interesting package that I discovered: https://github.com/zyedidia/generic. It covers a lot of data structures, do not hesitate to
take a look at it.
Let’s take an example. Let’s say you have to save some data in a database. We use for that DynamoDb, an AWS database solution.
// generics/dynamo/main.go
10 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
Here we want to save a product. And to save it into DynamoDb, we need to get the item’s partition key and sort key. Those two keys are
mandatory. So here, for the partition key, we use the string product and for the sort key, we use the product’s id.
I will need to create a new function to store it inside my DB. Because the first method is specific to the type product.
The solution here can be to create an interface. This interface will define a method to retrieve the partition key and the sort key :
We call the interface methods instead of relying on fields from the type. Then to use that function, we simply have to implement the interface
on the product and category types :
11 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
• https://github.com/samber/lo: a library that implements a lot of useful functions in the style of lodash (a famous javascript library)
• https://github.com/deckarep/golang-set: a library that provides a Set data structure that is fairly easy to use.
15 Test yourself
15.1 Questions
1. What does the character tilde ~ mean in ~int ?
3. The empty interface has been replaced in Go 1.18 by any . True or False?
4. When you call a generic function, you have to specify the type argument(s) you will use. Ex: I have to write myFunc[int, string](a,b) .
True or false?
5. Fill the blank. Let’s define the following function foo[T ~string](bar T) T , T is a ________ with a ________ denoted ~string .
6. Type parameters only exist for functions and methods. True or false?
12 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
9. Fill the blank. Generic functions may only _______ permitted by their type constraints.
15.2 Answers
1. What does the character tilde ~ mean in ~int ?
1. ~int denotes the int type by itself and also any named types whose underlying types are int
1. It means union
2. ~int | string means all strings OR the int type by itself and also any named types whose underlying types are int
3. The empty interface has been replaced in Go 1.18 by any . True or False?
1. False
2. It has not been replaced. Imagine if it were the case, it would have broken a lot of existing programs and libraries!
3. any is an alias to the empty interface; it means the same thing. You can use any instead of interface{} and vice versa.
4. When you call a generic function, you have to specify the type argument(s) you will use. Ex: I have to write myFunc[int, string](a,b) .
True or false?
2. When you provide the type parameters in your function call, you do not let the Go compiler infer those.
5. Fill the blank. Let’s define the following function foo[T ~string](bar T) T , T is a ________ with a ________ denoted ~string .
1. False; they also exist for types; you can build a generic type with Go.
7. A type constraint can be a type struct. True or False?
1. False.
1. A type constraint is an interface that defines the set of permissible type arguments for the respective type parameter and controls
the operations supported by values of that type parameter. (from the Go specs)
9. Fill the blank. Generic functions may only _______ permitted by their type constraints.
1. Generic functions may only use types permitted by their type constraints.
2. A type constraint allows only certain types; they constrain the types that can be used by a generic function/method/type.
The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.
16 Key Takeways
• Functions and methods can be generic.
• The type parameter list begins with open square brackets and ends with a closing square bracket.
13 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
• A type constraint is an interface that defines all the types you can use for a specific type parameter.
• The type constraint is here to answer the question: which type do I have the right to use here?
• Inside a type constraint, you can list all the types you allow the user of your function/method/type to use.
◦ You can use the pipe (|) character to make a union between type terms
◦ You can use the tilde character (~T) to denote the type T + all types whose underlying type is T.
▪ Example: ~int denotes the type int + all the types that have an underlying type equal to int
▪ Example: type DegreeC int, the type DegreeC has an underlying type equal to int. We say that’s a named type.
• When you call a generic function, you might need to provide the actual type of type parameter. But Go has the power to infer it. It is
checked at compile time.
◦ When you write the same function/method several times, the only thing that change is the input/output type.
◦ When you want to use a well-known data structure (ex: binary tree, HashSet,...), you will find existing implementations on the web.
◦ When you work with collection types like maps and slices, this is often (not always) a good use case.
• It would be best if you did not force generics into your code.
1. source: https://youtu.be/WzgLqE-3IhY?t=55↩
2. source: https://go.dev/project↩
3. See https://github.com/golang/go/issues/43651↩
4. https://www.youtube.com/watch?v=Pa_e9EeCdy8↩
Bibliography
• [jazayeri2003generic] Jazayeri, Mehdi, Rüdiger GK Loos, and David R Musser. 2003. Generic Programming: International Seminar on
Generic Programming Dagstuhl Castle, Germany, April 27-May 1, 1998, Selected Papers. Springer.
• [go-specs] “The Go Programming Language Specification.” n.d. Accessed April 30, 2018. https://golang.org/ref/spec.
Previous Next
Table of contents
Did you spot an error ? Want to give me feedback ? Here is the feedback page! ×
Newsletter:
Like what you read ? Subscribe to the newsletter.
14 of 15 02/01/2023, 02:23
Generics - Practical Go Lessons https://www.practical-go-lessons.com/chap-38-generics
@ my@email.com
Practical Go Lessons
By Maximilien Andile
Copyright (c) 2023
Follow me Contents
Posts
Book
Support the author Video Tutorial
About
The author
Legal Notice
Feedback
Buy paper or digital copy
Terms and Conditions
15 of 15 02/01/2023, 02:23