Understanding the Power of Generics in Go
Welcome to this post, where we’ll discuss Generics in Go, a powerful feature introduced in Go version 1.18.
Funtion
We can define a function that receives an integer value, for instance:
func Sum01(v int){
// TODO
}
If we want to receive different values in the function, we’ll define it as an interface.
func Sum01(v interface){
// TODO
}
Starting from version 1.18, we can use Generics for defining a set of data types, such as: int, int32, int64, float32, or float64.
To achieve this, we define a generic variable (in this case, N) and specify its set of data types within brackets.
The generic data types must be separated by the pipe character (|)
func Sum01[N int | int32 | int64 | float64 | float32]
After that, we define a generic data type in our function’s arguments. In this case, we receive an array of our generics.
func Sum01[N int | int32 | int64 | float64 | float32](v []N) {
}
We can also return our generic, where this generic (N) represents a set of data types (int, int32, int64, float64, float32).
In this case, we receive all array elements and perform their summation.
func Sum01[N int | int32 | int64 | float64 | float32](v []N) N{
var total N
for _, tV := range v{
total += tV
}
return total
}
We can execute the function with Println
to see the result.
v1 := []float64{1.3,5.45,12.223,6.92,78.102}
v2 := []int32{9,23,1,23,8,98}
fmt.Println(Sum01(v1))
fmt.Println(Sum01(v2))
Output
103.995
162
Interface
We can create an interface to define the data types that represent our generic.
We define an interface with the same data types, our generic is called ‘Number’.
type Number interface{
int | int32 | int64 | float64 | float32
}
And we define our generic with this interface instead of the data types.
func Sum02[N Number](v []N) N{
var total N
for _, tV := range v {
total += tV
}
return total
}
We execute the function
v1 := []float64{1.3,5.45,12.223,6.92,78.102}
v2 := []int32{9,23,1,23,8,98}
fmt.Println(Sum02(v1))
fmt.Println(Sum02(v2))
Output
103.995
162
Any
We can receive any values as parameters using the reserved word ‘any.’
In this case, we are receiving 2 generics as ‘any’ values (v1 and v2).
func anyType[N any](v1, v2 N){
fmt.Println(v1, v2)
}
we execute the function:
anyType(1,1)
anyType("1","1")
Output
1 1
1 1
We must take into account that if we send an integer value as a parameter, the generic will be an integer value. If we send a string value, the generic will be a string value. The generic will transform into the value we send. Therefore, we can’t send different data types as the same generic.
For example, in the previous example, we defined the generic N as ‘any
This generic (N) will be integer or string, but won’t be integer and string, if we execute this code the program will return error
anyType(1,"1")
Output
mismatched types untyped int and untyped string (cannot infer N)
If we wanted to send 2 different data types, we would define 2 generics:
func anyTypeTwo[N1 any, N2 any](v1 N1, v2 N2){
fmt.Println(v1, v2)
}
In this way, we can execute the function with integer and string values.
anyTypeTwo(1, "1")
Output
1 1
Comparable
We won’t perform comparisons between ‘any’ values. For instance, we won’t check if v1 is equal to v2.
func anyType[N any](v1, v2 N){
fmt.Println(v1, v2)
fmt.Println(v1 != v2)
}
Output
invalid operation: v1 != v2 (incomparable types in type set)
In this case, we must use the reserved word ‘comparable’ to compare whether the variables are equal.
func comparableType[N comparable](v1, v2 N){
fmt.Println(v1, v2)
fmt.Println(v1 != v2) // != or ==
}
We execute the function
comparableType(4,4)
Output:
4 4
false
Ordered
If we want to compare whether an ‘any’ type variable ‘a’ is greater or lesser than another variable ‘b,’ we must use the ‘Ordered’ interface in the internal ‘cmp’ package.
func orderedValues[N cmp.Ordered](v1, v2 N){
fmt.Println(v1, v2)
fmt.Println(v1 !=v2)
fmt.Println(v1 < v2)
fmt.Println(v1 > v2)
}
We execute the function
orderedValues(2,4)
Output:
2 4
true
true
false
The ‘cmp’ package was released in Go version 1.21. If your project is older than this version, you can use the external ‘constraints’ package.
go get golang.org/x/exp
import (
"fmt"
"golang.org/x/exp/constraints"
)
func orderedValues[N constraints.Ordered](v1, v2 N){
fmt.Println(v1, v2)
fmt.Println(v1 == v2)
fmt.Println(v1 < v2)
fmt.Println(v1 > v2)
}
Generics with slices
We can define, let’s say, a CustomSlice type with integer or string data types that represent a generic slice:
type CustomSlice[V int | string] []V
And we can use our custom slice:
csInt := CustomSlice[int]{1,2,3,4}
fmt.Println(csInt)
csStg := CustomSlice[string]{"a", "b","4"}
fmt.Println(csStg)
Output
[1 2 3 4]
[a b 4]
Tilde in generics
We are going to do the following examples.
We have our ‘Number’ interface that represents the following data types.
type Number interface{
int | int32 | int64 | float64 | float32
}
And we have a function like this, using our ‘Number’ interface, which returns the lesser value.
func MinNumber[T Number](x, y T) T {
if x < y {
return x
}
return y
}
We run the function, and it works perfectly
vv := MinNumber(5, 2)
fmt.Println(vv)
however, what happens if we define a type as integer called Point
type Point int
and we use this type to represent this integer value when we execute the function
x, y := Point(5), Point(2)
vv := MinNumber(x,y)
if we do it, the program will return the following error:
Point does not satisfy Number
In this interface we only can use int, int32, int64, float64, float32
type Number interface{
int | int32 | int64 | float64 | float32
}
But, if we want to use a type (such as Point) that represents these values, we use the tilde character (~).
type Number2 interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
The ~ tilde token is used in the form ~T to denote the set of types whose underlying type is T.
Now, we run the program and see the result
x, y := Point(5), Point(2)
vv := MinNumber(x,y)
fmt.Println(vv)
Output
2
Generics in structs
We are going to see how to use generics with structs.
First, we create those structs with their methods.
type MyFirstData struct { }
type MySecondData struct { }
func (d MyFirstData) PrintOne(){
fmt.Println("Print first")
}
func (d MySecondData) PrintTwo(){
fmt.Println("Print second")
}
And we create our generic struct with a field called ‘Data’ with the ‘any’ data type.
type MyGenericStruct[D any] struct {
StrValue string
Data D
}
Finally, we generate the struct and run the methods we defined for each of the structs that represent this ‘any’ field.
fd := MyGenericStruct[MyFirstData]{ StrValue: "Test", Data: MyFirstData{}}
fd.Data.PrintOne()
sd := MyGenericStruct[MySecondData]{ StrValue: "Test", Data: MySecondData{}}
sd.Data.PrintTwo()
Conclusion
We have seen how to use generics with some examples. This is a powerful functionality introduced in Go version 1.18. For more information, you can refer to the official documentation: https://go.dev/blog/intro-generics
repository: blog_go_generics
blog: Bee Blogit