We will see how to write generic functions, methods, and types
We will see how to use those generic constructs in your code
We will see the traditional use cases of generics: when to use them, when not to use them
Generic programming
Type constraints
Type parameters
Function
Method
Interface
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
\nIn this chapter, we will cover the topic of generics.
\n\nIf we refer to the Cambridge Dictionary, the adjective generic means: relating to or shared by a whole group of similar things; not specific to any particular thing.
\nHere 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..
\nIf we come back to programming, we can create, for instance, generic functions that will not bind to a specific type of input/output parameters.
\nWhen 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
.
Let’s take a simple example :
\n// generics/first/main.go\n\n// max function returns the maximum between two numbers\nfunc max(a, b int64) int64 {\n if a > b {\n return a\n }\n return b\n}
\nThis max function will only work if you input numbers of type int64 :
\n// generics/first/main.go\nvar a, b int64 = 42, 23\nfmt.Println(max(a, b))\n// 42
\nBut 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.
\n// generics/first/main.go\nvar c, d int32 = 12, 376\n// DOES NOT COMPILE\nfmt.Println(max(c, d))\n// ./main.go:10:18: cannot use c \n// (variable of type int32) as type int64 in argument to max\n// ./main.go:10:21: cannot use d \n// (variable of type int32) as type int64 in argument to max
\nThe 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.
\n\nWe can use this definition from
We now understand that generic programming aims to write interoperable and adaptable code. But what does it mean to have an interoperable code?
\nIt means that we want to be able to use the same function for different types that share some common capabilities.
\nLet’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\n\n// maxInt32 function works only for int32\nfunc maxInt32(a, b int32) int32 {\n if a > b {\n return a\n }\n return b\n}\n\n// maxUint32 function works only for uint32\nfunc maxUint32(a, b uint32) uint32 {\n if a > b {\n return a\n }\n return b\n}
\nWriting 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.
\nHere is the generic version of our max
function:
// generics/first/main.go\n\nfunc maxGeneric[T constraints.Ordered](a, b T) T {\n if a > b {\n return a\n }\n return b\n}
\nAnd we can use this function like that :
\n// generics/first/main.go\n\nfmt.Println(maxGeneric[int64](a, b))\n// 42\nfmt.Println(maxGeneric[int32](c, d))\n// 376
\nOr even like that :
\n// generics/first/main.go\n\nfmt.Println(maxGeneric(a, b))\nfmt.Println(maxGeneric(c, d))
\n\nDo you remember the empty interface:
\ninterface{}
\nAll 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 :
\n// generics/first/main.go\n\nfunc maxEmptyInterface(a, b interface{}) interface{} {\n if a > b {\n return a\n }\n return b\n}
\nCan I do that? No! The program will not compile. We will have the following error :
\n./main.go:64:5: invalid operation: a > b (operator > not defined on interface)
\nThe 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 :
\n>, ==, !=, <
\nWhen we write:
\nA > B
\nA
and B
are operands, and >
is the operator.
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.
Go developers added a new feature to the language named Type Parameters.
\nWe 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.
\n// generics/first/main.go\npackage main\n\nimport (\n "golang.org/x/exp/constraints"\n)\n\nfunc maxGeneric[T constraints.Ordered](a, b T) T {\n if a > b {\n return a\n }\n return b\n}
\nIn 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.
\nLet’s align on some vocabulary :
\n[T constraints.Ordered]
is the type parameter list.
T
is the type parameter identifier (or name)
constraints.Ordered
is the type constraint.
T constraints.Ordered
is a type parameter declaration.
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.
\nA type constraint (example constraints.Ordered
) 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.
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.
\nThis definition is a bit hard, but in reality, it’s not that complex; let’s decompose it:
\nIt should be an interface
In this interface, we define the set of permissible type arguments, all types we can use for this parameter.
It also dictates the operations supported by values of that type.
After seeing the theory, let’s take a look at what a type constraint looks like in reality :
\n// Ordered is a constraint that permits any ordered type: any type\n// that supports the operators < <= >= >.\n// If future releases of Go add new ordered types,\n// this constraint will be modified to include them.\ntype Ordered interface {\n Integer | Float | ~string\n}
\nSo 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 :
\nInteger | Float | ~string
\nYou 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 type term is either :
\nA single type
\nexample: string
, int
, Integer
It can be a predeclared type or another type
\nHere, 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.
Let’s take an example of that with the type Foo
:
type Foo interface {\n int | int8\n}
\nAn underlying type
\nexample: ~string
, ~uint8
You can note the additional tilde ~
here.
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
:
type DatabaseDSN string
\nThis type DatabaseDSN
is a new type, but the underlying type is string
, so as a consequence, this type fits into ~string
.
Let’s take another example to make sure you understand:
\ntype PrimaryKey uint
\nThis type PrimaryKey
is a new type, but the underlying type is uint
, so as a consequence, this type fits into ~uint
.
~uint
represents all types with an underlying type uint
.
We have the module golang.org/x/exp/constraints that we can use right away that contains useful type constraints :
\nYou will need first to import the package into your code with:
\ngo get golang.org/x/exp/constraints
\nThen you can use all the constraints.
\nSigned: all signed integers
\n~int | ~int8 | ~int16 | ~int32 | ~int64
ex: -10, 10
Unsigned: all signed integers
\n~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
ex: 42
Integer, the union of Signed and Unsigned
\nSigned | Unsigned
Float all floating point numbers
\n~float32 | ~float64
Complex all complex numbers
\n~complex64 | ~complex128
Ordered all types that we can order
\nInteger | Float | ~string
As you note here, this is the union between three types integers, float, and strings
Let’s take our example function maxGeneric again:
\nfunc maxGeneric[T constraints.Ordered](a, b T) T {...}
\nWe have seen in the previous example that to call our maxGeneric we needed to specify the argument type:
\nvar a, b int64 = 42, 23 \nmaxGeneric[int64](a, b)
\nIt 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.
\n\nIn the previous snippet, we used :
\nmaxGeneric[int64](a, b)
\nbut we can also directly write:
\nvar a, b int64 = 42, 23\nmaxGeneric(a, b)
\nIn 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.
\nNote 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.
\n\nLet’s take an example. Let’s say you want to create a specific type representing all maps with comparable keys and integer values.
\nWe can create a parametrized custom type :
\n// generics/types\ntype GenericMap[K constraints.Ordered, V constraints.Integer] map[K]V
\nWe 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).
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\n\nfunc (m GenericMap[K, V]) sum() V {\n var sum V\n for _, v := range m {\n sum = sum + v\n }\n return sum\n}
\nThen we can create two new variables of this type :
\n// generics/types\n\nm := GenericMap[string, int]{\n "foo": 42,\n "bar": 44,\n}\n\nm2 := GenericMap[float32, uint8]{\n 12.5: 0,\n 2.2: 23,\n}
\nAnd then we can use the feature!
\n// generics/types\n\nfmt.Println(m.sum())\n// 86\nfmt.Println(m2.sum())\n// 23
\nBut let’s say now I want to create a new variable of type map[string]uint
:
// generics/types\n\nm3 := map[string]uint{\n "foo": 10,\n}
\nCan I also benefit from the sum method? Can I do that :
\nfmt.Println(m3.sum())
\nThe 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)\nfmt.Println(m4.sum())
\nWe 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.
In the next two sections, I will replicate some advice given by Ian Lance Taylor in a talk given about generics when they came out4.
\n\nWhen 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!
\nLet’s take an example:
\n// generics/use-cases/same-fct2\n\nfunc containsUint8(needle uint8, haystack []uint8) bool {\n for _, v := range haystack {\n if v == needle {\n return true\n }\n }\n return false\n}\n\nfunc containsInt(needle int, haystack []int) bool {\n for _, v := range haystack {\n if v == needle {\n return true\n }\n }\n return false\n}
\nHere 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!
\n// generics/use-cases/same-fct2\n\nfunc contains[E constraints.Ordered](needle E, haystack []E) bool {\n for _, v := range haystack {\n if v == needle {\n return true\n }\n }\n return false\n}
\nWe have created a generic function named contains. This function has one type parameter of type constraints.Ordered
. This means we can compare the two elements of the slice because we can use the operator ==
with types that fulfill this constraint.
When manipulating collection types in function, methods, or types, you might need to use generic. Some libraries have emerged to propose you generic collection types. We will discover some libraries in another section.
\n\nTo understand this use case, we have to understand the definition of data structures (if you have never come across this) :
\nIf 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.
\nSo 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.
\nThe 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.
\nBut there is a lot more than maps :
\nThe linked list: each element in this list points to the next one
\nThe binary tree is a data structure that uses the graph theory, each element (called a node) has at most two children nodes.
\n... many more data structures exist in computer science
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.
\nThere 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.
\n\nSometimes you can use a basic interface to solve your problem, and using an interface makes your code easier to understand, especially for newcomers. Even if it was incomplete, Go before 1.18 already had a form of generic programming with interfaces. If you want a function to be used by ten types, check what you need those types to be capable of, then create an interface and implement it on your ten types.
\nLet’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.
\n// generics/dynamo/main.go\n\nfunc saveProduct(product Product, client *dynamodb.DynamoDB) error {\n marshalled, err := dynamodbattribute.MarshalMap(product)\n if err != nil {\n return fmt.Errorf("impossible to marshall product: %w", err)\n }\n marshalled["PartitionKey"] = &dynamodb.AttributeValue{\n S: aws.String("product"),\n }\n marshalled["SortKey"] = &dynamodb.AttributeValue{\n S: aws.String(product.ID),\n }\n input := &dynamodb.PutItemInput{\n Item: marshalled,\n TableName: aws.String(tableName),\n }\n _, err = client.PutItem(input)\n if err != nil {\n return fmt.Errorf("impossible to save item in db: %w", err)\n }\n return nil\n}
\nHere 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.
\nBut now, let’s say that I want to persist a category :
\ntype Category struct {\n ID string\n Title string\n}
\nI will need to create a new function to store it inside my DB. Because the first method is specific to the type product.
\nThe solution here can be to create an interface. This interface will define a method to retrieve the partition key and the sort key :
\ntype Storable interface {\n PartitionKey() string\n SortKey() string\n}
\nThen we can create a second version of our function.
\nfunc save(s Storable, client *dynamodb.DynamoDB) error {\n marshalled, err := dynamodbattribute.MarshalMap(s)\n if err != nil {\n return fmt.Errorf("impossible to marshall product: %w", err)\n }\n marshalled["PartitionKey"] = &dynamodb.AttributeValue{\n S: aws.String(s.PartitionKey()),\n }\n marshalled["SortKey"] = &dynamodb.AttributeValue{\n S: aws.String(s.SortKey()),\n }\n input := &dynamodb.PutItemInput{\n Item: marshalled,\n TableName: aws.String(tableName),\n }\n _, err = client.PutItem(input)\n if err != nil {\n return fmt.Errorf("impossible to save item in db: %w", err)\n }\n return nil\n}
\nWe 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 :
\ntype Product struct {\n ID string\n Title string\n}\n\nfunc (p Product) PartitionKey() string {\n return "product"\n}\n\nfunc (p Product) SortKey() string {\n return p.ID\n}\n\ntype Category struct {\n ID string\n Title string\n}\n\nfunc (c Category) PartitionKey() string {\n return "category"\n}\n\nfunc (c Category) SortKey() string {\n return c.ID\n}
\nAnd we can call save inside our program:
\nerr := saveProduct(teaPot, svc)\nif err != nil {\n panic(err)\n}\nerr = save(teaPot, svc)\nif err != nil {\n panic(err)\n}
\n\nIt makes sense to use generics when the implementation is the same, but when the implementation is different, you have to write different functions for each implementation. Do not force generics into your code!
\n\nHere is a non-exhaustive list of libraries where you can use some interesting generic function methods:
\nhttps://github.com/zyedidia/generic : Provides a wide range of data structures ready to use
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.
What does the character tilde ~
mean in ~int
?
What does the character pipe |
mean in ~int | string
?
The empty interface has been replaced in Go 1.18 by any
. True or False?
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?
Fill the blank. Let’s define the following function foo[T ~string](bar T) T
, T
is a ________ with a ________ denoted ~string
.
Type parameters only exist for functions and methods. True or false?
A type constraint can be a type struct. True or False?
Define the term type constraint.
Fill the blank. Generic functions may only _______ permitted by their type constraints.
What does the character tilde ~
mean in ~int
?
~int
denotes the int type by itself and also any named types whose underlying types are int
ex: type Score int
belongs to ~int
Score
is a named type, and its underlying type is int
What does the character pipe |
mean in ~int | string
?
It means union
~int | string
means all strings OR the int type by itself and also any named types whose underlying types are int
The empty interface has been replaced in Go 1.18 by any
. True or False?
False
It has not been replaced. Imagine if it were the case, it would have broken a lot of existing programs and libraries!
any
is an alias to the empty interface; it means the same thing. You can use any instead of interface{} and vice versa.
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?
This is usually false, but it can be true sometimes. Why?
When you provide the type parameters in your function call, you do not let the Go compiler infer those.
Go can infer those.
Fill the blank. Let’s define the following function foo[T ~string](bar T) T
, T
is a ________ with a ________ denoted ~string
.
T
is a type parameter with a type constraint denoted ~string
.Type parameters only exist for functions and methods. True or false?
\nA type constraint can be a type struct. True or False?
\nFalse.
A type constraint should be an interface.
Define the term type constraint.
\nFill the blank. Generic functions may only _______ permitted by their type constraints.
\nGeneric functions may only use types permitted by their type constraints.
A type constraint allows only certain types; they constrain the types that can be used by a generic function/method/type.
Functions and methods can be generic.
A function/method is generic if it provides a type parameter list.
The type parameter list begins with open square brackets and ends with a closing square bracket.
Generic function: func foo[E myTypeConstraint](myVar E) E { ... }
\nIn this function foo, we have one type parameter named E.
This type parameter is of type constraint myTypeConstraint
The type parameter is named E; this is its identifier.
This type parameter is used as an argument and as a result.
A type constraint is an interface that defines all the types you can use for a specific type parameter.
We designate the type constraint as a meta-type.
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.
\nYou can use the pipe (|) character to make a union between type terms
\nWe can understand union as a logical OR.
int|string means: int OR string
You can use the tilde character (~T) to denote the type T + all types whose underlying type is T.
\nExample: ~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.
We can also build generic types.
When should you think about generics ?
\nWhen 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.
Do not forget that you can still use pure interfaces!
Previous
\n\t\t\t\t\t\t\t\t\tContext
\n\t\t\t\t\t\t\t\tNext
\n\t\t\t\t\t\t\t\t\tAn object oriented programming language ?
\n\t\t\t\t\t\t\t\t