Generic Functions
Generics let you write one function that works for many types while keeping full type checking. Instead of writing SumInts, SumFloats and SumDurations, or giving up type safety with any, you write Sum[T Number] once. This lesson covers type parameters, constraints such as any, comparable and cmp.Ordered, type inference, and writing your own constraints.
The problem generics solve
Without generics, a function that finds the largest element has to be written once per type — or accept []any and lose all type checking. With a type parameter, one function serves every ordered type:
package main import ( "cmp" "fmt" ) func Largest[T cmp.Ordered](items []T) T { best := items[0] for _, v := range items[1:] { if v > best { best = v } } return best } func main() { fmt.Println(Largest([]int{3, 9, 4})) fmt.Println(Largest([]float64{2.5, -1, 0.75})) fmt.Println(Largest([]string{"kiwi", "apple", "plum"})) }
9 2.5 plum
[T cmp.Ordered] declares a type parameter T with a constraint: T may be any type that supports < and >. Inside the function, T is used like any other type. The compiler checks each call with the real type and generates efficient code.
Type inference and explicit instantiation
Usually the compiler works out T from the arguments, as above. When it can't — because the type parameter only appears in the result, say — you supply it in square brackets:
package main import "fmt" func Zero[T any]() T { var z T return z } func Repeat[T any](v T, n int) []T { out := make([]T, n) for i := range out { out[i] = v } return out } func main() { fmt.Println(Repeat("ab", 3), Repeat(7, 2)) // inferred fmt.Printf("%q %v\n", Zero[string](), Zero[float64]()) repeatBools := Repeat[bool] // instantiate without calling fmt.Println(repeatBools(true, 2)) }
[ab ab ab] [7 7] "" 0 [true true]
Built-in constraints
| Constraint | Allows | So you can use |
|---|---|---|
any | Every type | Assignment, passing around — no operators |
comparable | Types that support == and != | Equality tests, map keys |
cmp.Ordered | Integers, floats and strings | < <= > >= and == |
package main import "fmt" func Index[T comparable](items []T, target T) int { for i, v := range items { if v == target { return i } } return -1 } func Count[K comparable](items []K) map[K]int { counts := map[K]int{} for _, v := range items { counts[v]++ } return counts } func main() { fmt.Println(Index([]string{"a", "b", "c"}, "c")) fmt.Println(Count([]rune("banana"))) type Point struct{ X, Y int } fmt.Println(Index([]Point{{0, 0}, {2, 3}}, Point{2, 3})) }
2 map[97:3 98:1 110:2] 1
Using == on a T constrained only by any is a compile error; the constraint must promise that the operation is valid:
package main func Index[T any](items []T, target T) int { for i, v := range items { if v == target { return i } } return -1 } func main() {}
./main.go:5:6: invalid operation: v == target (incomparable types in type set)
Writing your own constraints
A constraint is an interface. Besides methods, it can list types with |. A ~ before a type means "this type or any type defined from it", so ~int also admits type UserID int:
package main import ( "fmt" "time" ) type Number interface { ~int | ~int32 | ~int64 | ~float32 | ~float64 } func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total } type Cents int64 func main() { fmt.Println(Sum([]int{1, 2, 3})) fmt.Println(Sum([]float64{0.5, 0.25})) fmt.Println(Sum([]Cents{199, 250})) // ~int64 admits Cents fmt.Println(Sum([]time.Duration{time.Second, time.Minute})) // Duration is ~int64 too }
6 0.75 449 1m1s
Interfaces that list types like this can only be used as constraints, not as ordinary variable types. The golang.org/x/exp/constraints module has ready-made ones such as Integer and Float, but small custom constraints are easy enough to write.
Constraints with methods
A constraint can also require methods. Here any type with a String method works:
package main import ( "fmt" "strings" ) func JoinAll[T fmt.Stringer](items []T, sep string) string { parts := make([]string, len(items)) for i, it := range items { parts[i] = it.String() } return strings.Join(parts, sep) } type Tag string func (t Tag) String() string { return "#" + string(t) } func main() { fmt.Println(JoinAll([]Tag{"go", "generics"}, " ")) }
#go #generics
Why not just take []fmt.Stringer? Because a []Tag isn't a []fmt.Stringer — you'd have to copy it into a new slice first. The generic version accepts the []Tag directly.
Several type parameters
package main import ( "fmt" "strconv" "strings" ) func Map[T, U any](items []T, f func(T) U) []U { out := make([]U, 0, len(items)) for _, v := range items { out = append(out, f(v)) } return out } func Filter[T any](items []T, keep func(T) bool) []T { var out []T for _, v := range items { if keep(v) { out = append(out, v) } } return out } func Reduce[T, A any](items []T, start A, f func(A, T) A) A { acc := start for _, v := range items { acc = f(acc, v) } return acc } func main() { nums := []int{1, 2, 3, 4, 5, 6} evens := Filter(nums, func(n int) bool { return n%2 == 0 }) labels := Map(evens, strconv.Itoa) fmt.Println(strings.Join(labels, "+"), "=", Reduce(evens, 0, func(a, n int) int { return a + n })) }
2+4+6 = 12
When to use generics
Generics shine for data structures (lists, sets, caches, trees) and for functions over collections that don't care about the element type — exactly what the slices and maps packages provide. Don't reach for them first: if a function only needs some behaviour, an ordinary interface parameter is simpler, and if it only needs to work for one type, just write it for that type.
Try it yourself
Write a generic Unique that removes duplicates from a slice while keeping the original order. Test it with strings and ints.
[go js py] [3 1 2]