Kodewick
Start learning →
Lesson 17 of 24Contents

Generic functions

Lesson 17 of 24 · Part 4 · Errors and functions · about 15 minutes

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:

prog.go
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}
Output
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:

prog.go
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}
Output
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:

prog.go
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}
Output
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:

prog.go
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}
Output
# command-line-arguments
./prog.go:18:25: in call to Max, type float64 of rate does not match inferred type int for T

price 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:

prog.go
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}
Output
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.

Guess first

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}

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:

prog.go
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}
Output
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:

prog.go
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}
Output
# 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):

prog.go
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}
Output
[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:

prog.go
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}
Output
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.

Exercise 1

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.)

It should print
7
apple
exercise1.go
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}
Output
Press Run or Ctrl+Enter
Exercise 2

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.

It should print
3
2
exercise2.go
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}
Output
Press Run or Ctrl+Enter
Exercise 3

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.

It should print
60
7.5
exercise3.go
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}
Output
Press Run or Ctrl+Enter

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 T may be: cmp.Ordered (can use < >), any (every type).
  • Go usually infers T from the arguments; Max[float64](3, 4) gives it explicitly; mixed types don't fit one T.
  • 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 slices package (Sort, Max, Min, Contains, Index, Reverse) is written with generics.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.