Now we continue learning about generics. In this topic, you'll learn how to use an interface to define a set of constraints, how to satisfy constraints on custom-defined types, and how the Go compiler infers the intended data types for generic functions. You will also learn about a new generic type that supports comparison operators and get to know how to create a function with multiple generic types.
Defining a set of constraints using an interface
As you know, we can create combined type constraints via the func foo[T int | float64 | complex128] syntax. However, we can also define a set of constraints using an interface:
// Number expresses a type constraint satisfied by the most common numeric types
type Number interface {
int | float64 | complex128
}
After creating the Number interface, we can write a much shorter generic function declaration:
func foo[T Number](a []T) []T {...}
func foo[T Number] would be a generic function that could only take numeric type arguments defined in the Number interface — int, float64, and complex128.
Type approximation
In Go, we can create custom user-defined types from built-in types like int, float64, string, etc. Within a type constraint declaration, we can use the tilde ~ operator on a built-in type to express a constraint that can be satisfied by:
- a defined or named type;
- a type definition with the same underlying type as another defined or named type.
For example, ~int would match both the built-in int type and a new custom-defined type, such as type id int.
To further explain how the tilde ~ operator works, suppose we have the following Go program:
...
type id int // 'id' is a custom-defined type of the built-in 'int' type
type Number interface {
// Add ~ to 'int' to satisfy the custom-defined type 'id'
~int | float64 | complex128
}
// sum returns the total sum of zero to many numeric values
func sum[T Number](nums ...T) T {
var total T
for _, num := range nums {
total += num
}
return total
}
func main() {
fmt.Println(sum([]id{1, 2, 3}...)) // 6
}
As you can see, the ~int syntax allows using the custom id type as a valid argument for the sum() function. However, if we removed ~ from int within the Number constraint declaration and then run the program, we would get the following error:
id does not implement Number (possibly missing ~ for int in constraint Number)
~ operator also works on explicitly declared type constraints:
func sum[T ~int | float64 | complex128](nums ...T) T {...}
Type inference
One of the convenient features that generic functions provide is type inference. It means that the Go compiler can attempt to infer the intended, concrete types we pass as arguments, for example:
...
func main() {
// Providing the type argument [float64] to the 'sum()' function:
fmt.Println(sum[float64](3.1416, 1.618, 2.7182)) // 7.4778
// Making the compiler infer the type argument for the 'sum()' function:
fmt.Println(sum(3.1416, 1.618, 2.7182)) // 7.4778
}
As you can see, even though we don't provide the [float64] type argument to the sum() function in the second call, the Go compiler infers correctly that we're passing float64 type arguments to sum().
An important detail is that the Go compiler tries really hard to infer the intended data types, however, it doesn't always work properly:
....
func main() {
fmt.Println(sum(10, 9, 8.0))
}
// Error: default type float64 of 8.0 does not match inferred type int for T
Since we have passed both int - (10, 9) and float64 - (8.0) types to the sum() function, the Go compiler is not able to infer the intended data type and we get the does not match inferred type int for T error.
Despite type inference being a convenient feature, on certain occasions you'll need to explicitly provide the intended data type argument for every generic function call.
The comparable type constraint
Go version 1.18 introduces the comparable type, which is an interface that all comparable types implement, such as booleans, numbers, strings, arrays of comparable types, and structs whose fields are all comparable types as well.
comparable is a built-in type, take notice that it may only be used as a type constraint! You can't use it as the type of a variable.A common use case for the comparable type constraint is when a generic function needs to compare two variables. For example, a function that returns the index of a certain element in a slice:
func indexOf[T comparable](a []T, v T) int {
for i, e := range a {
if e == v {
return i
}
}
return -1
}Multiple generic types
So far, we've only seen generic functions that use a single generic type parameter T, however, we can also create a generic function that has multiple generic types as parameters.
To further explain the concept of a generic function with multiple generic types, let's create a new generic function sumValues(). It will receive a map with keys of the comparable type and values of the Number type. Then it will sum all the values of the map, and finally, return the total:
...
// Add two generic type parameters 'K' and 'V':
func sumValues[K comparable, V Number](m map[K]V) V {
var total V
for _, v := range m {
total += v
}
return total
}
When declaring multiple generic type parameters, you should keep in mind using the correct data type for each generic parameter we declare. For example, we wouldn't be able to use any as the type of K in the above function, because a map key in Go must be of the comparable type to allow values that support the comparison operators == and !=.
Finally, we can call sumValues() and see the output:
...
func main() {
intMap := map[string]int{"one": 1, "two": 2, "three": 3}
floatMap := map[string]float64{"pi": 3.14, "golden ratio": 1.618, "Euler's number": 2.718}
fmt.Println(sumValues[string, int](intMap)) // 6
fmt.Println(sumValues(floatMap)) // 7.476
}Conclusion
In this topic, you've learned about advanced use cases for generic functions in Go. In particular, you've covered the following theory:
- How to define a set of constraints using an interface;
- How to use the tilde
~operator on constraints to satisfy custom user-defined types; - Certain gotchas regarding type inference when working with generic functions;
- That the
comparableinterface allows values that support the comparison operators==and!=; - How to create a generic function with multiple generic types.
Enough reading! Let's go ahead and solve some tasks to test your knowledge of generics in Go.