Generic functions
Here are two functions that do the same thing, one for int and one for float64. Go won't let one function take both types, so the code is written twice:
1package main2 3import "fmt"4 5func maxInt(a, b int) int {6 if a > b {7 return a8 }9 return b10}11 12func maxFloat(a, b float64) float64 {13 if a > b {14 return a15 }16 return b17}18 19func main() {20 fmt.Println(maxInt(3, 7))21 fmt.Println(maxFloat(2.5, 1.5))22}
7 2.5
And for strings it would be a third copy. Generics let you write it once.
Type parameters
A generic function has a type parameter in square brackets before its normal parameters. [T cmp.Ordered] says: "T stands for some type, and it has to be one that can be ordered with < and >". Then T is used like any other type:
1package main2 3import (4 "cmp"5 "fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9 if a > b {10 return a11 }12 return b13}14 15func main() {16 fmt.Println(Max(3, 7))17 fmt.Println(Max(2.5, 1.5))18 fmt.Println(Max("apple", "banana"))19}
7 2.5 banana
cmp.Ordered is a constraint: the list of types T is allowed to be (all the integer and float types, and string). In each call, T becomes one of them: int, float64, string.
Go works out T
Usually you don't say what T is; Go infers it from the arguments. You can also give it yourself in square brackets:
1package main2 3import (4 "cmp"5 "fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9 if a > b {10 return a11 }12 return b13}14 15func main() {16 fmt.Println(Max[float64](3, 4))17 fmt.Printf("%T\n", Max[float64](3, 4))18 fmt.Println(Max(2, 2.5))19}
4 float64 2.5
Max(2, 2.5) works too: plain numbers have no fixed type yet, and Go picks float64 for both. Variables do have a fixed type, though, and both arguments have to agree on one T:
1package main2 3import (4 "cmp"5 "fmt"6)7 8func Max[T cmp.Ordered](a, b T) T {9 if a > b {10 return a11 }12 return b13}14 15func main() {16 price := 1017 rate := 1.518 fmt.Println(Max(price, rate))19}
# command-line-arguments
./prog.go:18:25: in call to Max, type float64 of rate does not match inferred type int for Tprice is an int and rate a float64, so no single T fits both: type float64 of rate does not match inferred type int for T. Go took T from the first argument, then found the second didn't match. Write Max(float64(price), rate) to fix it.
any: works for every type
When a function doesn't need to compare or add its values, the constraint can be any, which every type satisfies:
1package main2 3import "fmt"4 5func Last[T any](items []T) T {6 return items[len(items)-1]7}8 9func Repeat[T any](v T, n int) []T {10 result := make([]T, n)11 for i := range n {12 result[i] = v13 }14 return result15}16 17func main() {18 fmt.Println(Last([]string{"milk", "bread", "eggs"}))19 fmt.Println(Last([]float64{36.6, 37.1}))20 fmt.Println(Repeat("hi", 3))21 fmt.Println(Repeat(true, 2))22}
eggs 37.1 [hi hi hi] [true true]
But with any, Go can't let you use > or + on T: some types (a struct, a slice) don't have them.
What happens?
1package main2 3import "fmt"4 5func Biggest[T any](a, b T) T {6 if a > b {7 return a8 }9 return b10}11 12func main() {13 fmt.Println(Biggest(3, 9))14}
T could be any type, including ones without >, so Go refuses to compile a > b. The constraint has to promise that > works: cmp.Ordered.
Your own constraints
A constraint is an interface that lists types, separated by |. A ~ in front means "this type, or any type built on it", such as type Celsius float64:
1package main2 3import "fmt"4 5type Number interface {6 ~int | ~float647}8 9type Celsius float6410 11func Sum[T Number](nums []T) T {12 var total T13 for _, n := range nums {14 total += n15 }16 return total17}18 19func main() {20 fmt.Println(Sum([]int{120, 35, 8}))21 fmt.Println(Sum([]float64{2.5, 0.25}))22 fmt.Println(Sum([]Celsius{20.5, 21}))23}
163 2.75 41.5
var total T is T's zero value, 0 for every number type. Calling Sum with a type that isn't in the list is a compile error that names the type:
1package main2 3import "fmt"4 5type Number interface {6 ~int | ~float647}8 9func Sum[T Number](nums []T) T {10 var total T11 for _, n := range nums {12 total += n13 }14 return total15}16 17func main() {18 fmt.Println(Sum([]string{"a", "b"}))19}
# command-line-arguments
./prog.go:18:17: string does not satisfy Number (string missing in ~int | ~float64)string does not satisfy Number: string isn't int or float64, or built on them.
Two type parameters
A function can have more than one type parameter. Map turns a []T into a []U with a function you give it (the function values from lesson 16):
1package main2 3import (4 "fmt"5 "strings"6)7 8func Map[T, U any](items []T, f func(T) U) []U {9 result := make([]U, 0, len(items))10 for _, item := range items {11 result = append(result, f(item))12 }13 return result14}15 16func main() {17 names := []string{"ana", "ben", "carla"}18 fmt.Println(Map(names, strings.ToUpper))19 fmt.Println(Map(names, func(s string) int { return len(s) }))20}
[ANA BEN CARLA] [3 3 5]
The first call turns strings into strings; the second turns strings into ints, so U is int.
The slices package
You've already used generic functions: slices.Sort in lesson 8 was one. The slices package has more, and all of them work for any slice type:
1package main2 3import (4 "fmt"5 "slices"6)7 8func main() {9 scores := []int{72, 95, 58, 88}10 fmt.Println(slices.Max(scores), slices.Min(scores))11 fmt.Println(slices.Contains(scores, 58), slices.Index(scores, 88))12 words := []string{"pear", "fig", "apple"}13 fmt.Println(slices.Max(words), slices.Index(words, "kiwi"))14 slices.Reverse(words)15 fmt.Println(words)16}
95 58 true 3 pear -1 [apple fig pear]
slices.Index returns -1 when the value isn't there. Before you write a loop over a slice, check whether slices already has it.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
The smallest of any slice
✓Finish MinOf so it returns the smallest element of items, for any ordered type. The program tries it with numbers and with words. (Write the loop yourself, without slices.Min.)
7 apple
1package main2 3import (4 "cmp"5 "fmt"6)7 8func MinOf[T cmp.Ordered](items []T) T {9 return items[0]10}11 12func main() {13 fmt.Println(MinOf([]int{42, 7, 19}))14 fmt.Println(MinOf([]string{"pear", "fig", "apple"}))15}
Count the matches
✓Finish the generic function Count: return how many elements of items make match return true. It's called with ints and with strings.
3 2
1package main2 3import "fmt"4 5func Count[T any](items []T, match func(T) bool) int {6 return 07}8 9func main() {10 temps := []int{18, 25, 31, 22, 29}11 fmt.Println(Count(temps, func(t int) bool { return t > 24 }))12 names := []string{"Ana", "Ben", "Amir"}13 fmt.Println(Count(names, func(s string) bool { return s[0] == 'A' }))14}
Fix it: Sum that only takes ints
✓This program doesn't compile: the constraint Number only allows int. Change the constraint so Sum also works for float64.
60 7.5
1package main2 3import "fmt"4 5type Number interface {6 ~int7}8 9func Sum[T Number](nums []T) T {10 var total T11 for _, n := range nums {12 total += n13 }14 return total15}16 17func main() {18 fmt.Println(Sum([]int{10, 20, 30}))19 fmt.Println(Sum([]float64{2.5, 5}))20}
What you learned
- A generic function has type parameters in square brackets:
func Max[T cmp.Ordered](a, b T) T. - A constraint says which types
Tmay be:cmp.Ordered(can use<>),any(every type). - Go usually infers
Tfrom the arguments;Max[float64](3, 4)gives it explicitly; mixed types don't fit oneT. - Your own constraint lists types:
interface { ~int | ~float64 };~also allows types built on them. - A function can have several type parameters:
Map[T, U any]. - The
slicespackage (Sort,Max,Min,Contains,Index,Reverse) is written with generics.