Generics help us to avoid redundant declarations of functions by allowing us to specify type parameters for the data being passed to the function. Consider the following code:
package main
import "fmt"
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
func main() {
ints := map[string]int64{
"first": 10,
"second": 12,
}
floats := map[string]float64{
"first": 12.34,
"second": 15.52,
}
fmt.Println("sum of ints:", SumInts(ints))
fmt.Println("sum of floats:", SumFloats(floats))
}
The functions SumInts and SumFloats are used to find the sum of the integers and floats respectively passed via maps into them. This might become even more redundant if we have to handle other numeric data types.
We can replace these two functions with a single generic function that handles multiple numeric data types:
package main
import "fmt"
func SumNumbers[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
func main() {
ints := map[string]int64{
"first": 10,
"second": 12,
}
floats := map[string]float64{
"first": 12.34,
"second": 15.52,
}
fmt.Println("sum of ints:", SumNumbers[string, int64](ints))
fmt.Println("sum of floats:", SumNumbers[string, float64](floats))
fmt.Println("sum of ints (inferred):", SumNumbers(ints))
fmt.Println("sum of floats (inferred):", SumNumbers(floats))
}
The SumNumbers function has two ($2$) type parameters (K and V) specified in the square brackets. The comparable type constraint caters for any type whose value can be used with == and !=. This makes it suitable for use in a map as keys because Go specifies that map keys be comparable.
The type constraint for the type parameter V is a union of two ($2$) types: int64 and float64. We use the pipe (|) character to specify a union, thus allowing either numeric type to be used by the function.
The parameter m is of type map[K]V which will be a valid map type because K is comparable.
Note that we can explicitly specify the type parameters when calling the function (e.g. SumNumbers[string, int64](ints)) or we can simply have Go infer the types parameters for us (e.g. SumNumbers(ints)).
Using a custom type constraint
We can declare an interface Number and specify the union inside of the interface:
package main
import "fmt"
type Number interface {
int64 | float64
}
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
func main() {
ints := map[string]int64{
"first": 10,
"second": 12,
}
floats := map[string]float64{
"first": 12.34,
"second": 15.52,
}
fmt.Println("sum of ints:", SumNumbers[string, int64](ints))
fmt.Println("sum of floats:", SumNumbers[string, float64](floats))
fmt.Println("sum of ints (inferred):", SumNumbers(ints))
fmt.Println("sum of floats (inferred):", SumNumbers(floats))
}
This allows us to have a shorter and more scalable form of our SumNumbers function as we can easily append more data types to the union.