Generics

3-minute read
Table of Contents

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))
}
generics-1.go
Copy

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))
}
generics-2.go
Copy

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))
}
generics-3.go
Copy

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.

Support us via BuyMeACoffee

Resources

Here are some resources we recommend for you to use along with this lesson: