DMarlon Studio

Lesson 8 · 8 of 20

Generics: type parameters and constraints · Go

Write generic functions in Go with type parameters and constraints: one Sum for several number types and one Map for any type.

Go · Generics: type parameters and constraints in DMarlon Studio: the example's code and its output in the terminal

Since Go 1.18 a function can have type parameters, in square brackets before the ordinary ones: Sum[T Number](values []T) T. T is a type chosen at the call, and the same function adds up integers, decimals and cents without writing it three times or falling back on any.

Number is a constraint: an interface that, instead of methods, lists the allowed types with |. Thanks to it the compiler knows they all support +=, so total += v is allowed. The tilde in `~int64` means int64 or any type whose underlying type is int64, which is why Cents gets in too.

var total T makes the zero value of T: 0 for numbers. At the call, Go infers T from the arguments: with []int{3, 5, 8}, T is int. With nil there is nothing to infer it from, so you spell it out: Sum[float64](nil) returns 0.

Map[T, U any] has two type parameters and the `any` constraint, which accepts every type but lets you do almost nothing with it: store it, pass it and return it. That is enough to apply f to each item: from []int to []string ([#7 #12 #30], and %T confirms []string) and from there to []int with the lengths.

To compare values with == there is the `comparable` constraint. In Go 1.19 the standard library has none for < and >; you can declare your own just like Number, with the types you need.

Code · main.go

package mainimport "fmt"// Number is a constraint: the set of types that T may be.type Number interface{ ~int | ~int64 | ~float64 }type Cents int64 // ~int64 also lets in named types such as Centsfunc Sum[T Number](values []T) T {	var total T // the zero value of T	for _, v := range values {		total += v	}	return total}// Map works for any input type T and any output type U.func Map[T, U any](items []T, f func(T) U) []U {	out := make([]U, 0, len(items))	for _, item := range items {		out = append(out, f(item))	}	return out}func main() {	fmt.Println(Sum([]int{3, 5, 8}), Sum([]float64{2.5, 1.25}), Sum([]Cents{1999, 450}))	fmt.Println(Sum[float64](nil)) // explicit type argument	labels := Map([]int{7, 12, 30}, func(n int) string { return fmt.Sprintf("#%d", n) })	fmt.Printf("%v %T\n", labels, labels)	fmt.Println(Map(labels, func(s string) int { return len(s) }))}

Output · go run main.go

16 3.75 2449
0
[#7 #12 #30] []string
[2 3 3]

Key points

  • Type parameters go in square brackets: func Sum[T Number].
  • A constraint is an interface that says which types are allowed.
  • ~int64 also lets in types defined on top of int64.
  • Go infers T from the arguments; if it cannot, write Sum[float64].
  • any accepts every type; comparable, those you can compare with ==.

Exercise

Write Filter[T any](items []T, keep func(T) bool) []T and use it to keep the amounts in []Cents{1999, 450, 12000} greater than 1000. Then add up the result with Sum.