Go Module 11 – Generics, including generic methods (1.27)
Constraints, type sets and cmp.Ordered
Write Go constraints with unions, the ~ that admits named types, and methods; use cmp.Ordered correctly, and predict which operators a type parameter allows.
What you will learn
- Write a constraint interface with a union of types and the ~ approximation
- Predict which operators a type parameter allows from its type set
- Order values of a type parameter with cmp.Ordered and cmp.Compare, NaN included
- Combine a type set with methods in one constraint, and keep a caller's slice type with S ~[]E
Before you start
What you’ll learn · Before you start
What you will learn
- Write a constraint interface with a union of types and the ~ approximation
- Predict which operators a type parameter allows from its type set
- Order values of a type parameter with cmp.Ordered and cmp.Compare, NaN included
- Combine a type set with methods in one constraint, and keep a caller's slice type with S ~[]E
Before you start
On this page
Sum([]Rupees{100, 250}) should print 350, yet under the constraint int | float64 the call does not compile.
Rupees is a type of its own whose underlying type is int, and the constraint lists int alone.
A constraint is an interface, and the types it accepts are its type set. You will write constraints with unions and
the ~ that admits named types, see which operators each one allows, and order values with cmp.Ordered.
A constraint’s type set lists the types it accepts
Here is the failing call. The compiler’s message already suggests the fix:
// missing_tilde.go: int | float64 lists two types, and Rupees is neither of them.
package main
import "fmt"
// Rupees is an amount in whole rupees.
type Rupees int
// Sum adds up xs.
func Sum[T int | float64](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
func main() {
fmt.Println(Sum([]Rupees{100, 250}))
} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./missing_tilde.go:19:17: Rupees does not satisfy int | float64 (possibly missing ~ for int in int | float64)
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run missing_tilde.go
Go’s constraints are interfaces, and the set of types that satisfy one is its type set: The set of types that satisfy an interface. For a constraint such as ~int | ~float64 it is every type whose underlying type is int or float64, and a type argument compiles only if it is in the set..
int | float64 is a union: A constraint term of the form A | B | C, whose type set holds every type that matches one of its terms, such as int | float64 or ~int | ~int64. of two terms, and its type set holds exactly those two types. A
term written ~int means every type whose underlying type: The type a defined type is built on: for type Rupees int it is int, and for type Fares []Paise it is []Paise. A ~T term in a constraint matches every type whose underlying type is T. is int: int itself,
Rupees, and any other type declared as type X int. Number accepts all of those, and the int64 and float64 families
as well:
// sum_rupees.go: a Number constraint with ~ accepts Rupees and Paise, types of their own.
package main
import "fmt"
// Rupees is an amount in whole rupees.
type Rupees int
// Paise is an amount in paise.
type Paise int64
// Number is every type whose underlying type is int, int64 or float64.
type Number interface {
~int | ~int64 | ~float64
}
// Sum adds up xs.
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
func main() {
fmt.Println(Sum([]Rupees{100, 250}))
fmt.Println(Sum([]Paise{54500, 9900}))
fmt.Println(Sum([]float64{2.5, 1.25}))
fmt.Printf("%T\n", Sum([]Rupees{100, 250}))
} Output
350 64400 3.75 main.Rupees
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run sum_rupees.go
Sum returned 350 of type main.Rupees, so the caller got its own type back, and Paise, declared on int64, was added
the same way. Step through the checks the compiler made for both constraints:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An exact union. The constraint int | float64 is a union of two terms without a tilde, so its type set holds exactly two types, int and float64.
- Rupees is refused. Rupees is declared as type Rupees int. Its underlying type is int, but Rupees itself is a different type, neither int nor float64, so Sum([]Rupees{100, 250}) does not compile and the compiler suggests a missing ~.
- A union of ~ terms. Number writes each term with a tilde: ~int stands for every type whose underlying type is int, and the same for int64 and float64.
- Rupees is accepted. Rupees has the underlying type int, so it is in ~int's part of the set, and Sum returned 350 of type main.Rupees.
- Paise and float64 too. Paise, declared on int64, falls under ~int64, and float64 under ~float64, because every type is its own underlying type. sum_rupees.go added both.
The same rule decides every type argument. Which ones each constraint takes:
| Type argument | int or float64 | Number | cmp.Ordered |
|---|---|---|---|
int |
yes | yes | yes |
Rupees, declared on int |
no | yes | yes |
float64 |
yes | yes | yes |
string |
no | no | yes |
[]byte |
no | no | no |
Checkpoint
Check: Type sets, unions and ~
An operator compiles on T only if every type in the set has it
Type sets also decide what the code inside a generic function may do with a T: an operator is allowed only if every type in the set supports it. operators.go asks for two that fail:
// operators.go: an operator compiles on T only if every type in T's type set has it.
package main
// Number is every type whose underlying type is int, int64 or float64.
type Number interface {
~int | ~int64 | ~float64
}
// Paisa returns the paise part of an amount.
func Paisa[T Number](x T) T {
return x % 100
}
// Larger returns the larger of a and b.
func Larger[T comparable](a, b T) T {
if a < b {
return b
}
return a
}
func main() {} Output (exit status 1)
Printed as an error (standard error)
# command-line-arguments ./operators.go:11:9: invalid operation: operator % not defined on x (variable of type T constrained by Number) ./operators.go:16:5: invalid operation: a < b (type parameter T cannot use operator <)
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run operators.go
% is not defined on floating-point values, and float64 is in Number’s set, so x % 100 is refused even though most
calls would pass integers. The table shows four constraints against four operators:
| Constraint | == |
< |
+ |
% |
|---|---|---|---|---|
any |
no | no | no | no |
comparable |
yes | no | no | no |
Number |
yes | yes | yes | no |
cmp.Ordered |
yes | yes | yes, joins strings | no |
< is not part of comparable at all: comparable promises == and !=, and structs,
pointers and interfaces have no order. The constraint for <, <=, > and >= is
cmp.Ordered: The constraint of package cmp, added in Go 1.21, whose type set holds every type with an integer, floating-point or string underlying type, so code may use <, <=, > and >= on its values., added in Go 1.21, whose type set holds every type with an integer,
floating-point or string underlying type. MaxBy orders trains by any such key:
// max_by.go: cmp.Ordered allows <, so MaxBy can order trains by any ordered key.
package main
import (
"cmp"
"fmt"
)
// Train is one train of a timetable.
type Train struct {
No string
Name string
Paise int64
}
// MaxBy returns the item of xs with the largest key, the first one on a tie.
func MaxBy[T any, K cmp.Ordered](xs []T, key func(T) K) T {
best := xs[0]
for _, x := range xs[1:] {
if key(x) > key(best) {
best = x
}
}
return best
}
func main() {
trains := []Train{
{No: "10002", Name: "Coast Express", Paise: 54500},
{No: "10003", Name: "Hill Mail", Paise: 120000},
{No: "10001", Name: "Lake Express", Paise: 9900},
}
fmt.Println(MaxBy(trains, func(t Train) int64 { return t.Paise }))
fmt.Println(MaxBy(trains, func(t Train) string { return t.Name }))
fmt.Println(MaxBy([]float64{2.5, 7.25, 7}, func(x float64) float64 { return x }))
} Output
{10003 Hill Mail 120000}
{10001 Lake Express 9900}
7.25
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run max_by.go
The highest fare picked Hill Mail; the last name in alphabetical order picked Lake Express; and the plain float64 values gave 7.25. Constraints can do more than list types, and type sets decide what each one allows:
- A constraint can require methods as well as types, as
interface{ ~int64; String() string }does. A type argument then needs both: an underlying type in the set and every method listed. - A constraint written
S ~[]Enames a slice type and its element type apart, so a function returns the caller’s own slice type. cmp.Compareputs floating-point NaN in one fixed place, where<and>report false for every comparison with it.
Key idea
Code inside a generic function may use only what every type in its constraint’s type set supports: + under
Number, == under comparable, and < under cmp.Ordered.
Checkpoint
Check: Operators a type set allows
Constraints with methods, and slice types kept as S ~[]E Pro: opens with a Pro, Premium or Ultimate pass
A constraint can ask for methods and types at once, and it can take a slice type apart into the slice and its elements. This part shows both in programs that ran, and the three constraints the compiler refuses outright.
In this part:
- A type set and a method in one constraint
- Keeping the caller’s slice type with S ~[]E
- Three constraints the compiler refuses
5 runnable examples (Go) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Ordering with cmp: NaN, several keys, and operations across a type set Pro: opens with a Pro, Premium or Ultimate pass
Every float type in cmp.Ordered can hold NaN, a value that is neither below, above nor equal to anything. This part reproduces a maximum that depends on the order of its input, fixes it with cmp.Compare, sorts by several keys, and shows which operations a type set of mixed types allows.
In this part:
- A maximum that depends on the order of its input
- Sorting by several keys with cmp.Or
- Indexing and len across a mixed type set
4 runnable examples (Go) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: constraints for a clamp, a sum and a median
practice/numbers.go has three functions to write, each with a constraint from this lesson, and its tests use a named type, rupees, and empty input:
Go (numbers.go) · practice/numbers.go
// Package practice holds three generic functions for you to write.
package practice
import "cmp"
// Number is every type whose underlying type is int, int64 or float64.
type Number interface {
~int | ~int64 | ~float64
}
// Clamp returns v, raised to lo if it is below lo and lowered to hi if it is above hi.
func Clamp[T cmp.Ordered](v, lo, hi T) T {
return v // write the comparisons
}
// SumBy adds up f(x) for every x of xs.
func SumBy[T any, N Number](xs []T, f func(T) N) N {
var total N
return total // write the loop
}
// Median returns the middle value of xs once sorted (the mean of the two middle values when len(xs) is even) and
// true, or the zero value and false when xs is empty. It does not change xs.
func Median[T ~int | ~float64](xs []T) (T, bool) {
var zero T
return zero, false // sort a copy, then pick the middle
} Go (numbers_test.go) · practice/numbers_test.go
package practice
import "testing"
type rupees int
type item struct {
Name string
Qty int
Each rupees
}
func TestClamp(t *testing.T) {
if got := Clamp(rupees(1500), 100, 1000); got != 1000 {
t.Errorf("Clamp(1500, 100, 1000) = %d, want 1000", got)
}
if got := Clamp(2.5, 3.0, 4.0); got != 3 {
t.Errorf("Clamp(2.5, 3, 4) = %v, want 3", got)
}
if got := Clamp("MAS", "NDLS", "SBC"); got != "NDLS" {
t.Errorf(`Clamp("MAS", "NDLS", "SBC") = %q, want "NDLS"`, got)
}
}
func TestSumBy(t *testing.T) {
cart := []item{{"rice", 2, 60}, {"dal", 1, 140}, {"tea", 3, 45}}
if got := SumBy(cart, func(it item) rupees { return rupees(it.Qty) * it.Each }); got != 395 {
t.Errorf("total of the cart = %d, want 395", got)
}
if got := SumBy([]item{}, func(it item) int { return it.Qty }); got != 0 {
t.Errorf("SumBy of no items = %d, want 0", got)
}
}
func TestMedian(t *testing.T) {
fares := []rupees{1200, 99, 545}
if got, ok := Median(fares); got != 545 || !ok {
t.Errorf("Median(%v) = %d, %v, want 545, true", fares, got, ok)
}
if fares[0] != 1200 {
t.Errorf("Median changed its input: %v", fares)
}
if got, ok := Median([]float64{4, 1, 3, 2}); got != 2.5 || !ok {
t.Errorf("Median of 4, 1, 3, 2 = %v, %v, want 2.5, true", got, ok)
}
if _, ok := Median([]int{}); ok {
t.Errorf("Median of no values reported a value")
}
} from session import run
# The starter code of practice/ compiles, and every test fails until the three functions are written.
run(
"""
cd practice && go test .
""",
expect_failure=["cd practice && go test ."],
) Output
$ cd practice && go test .
--- FAIL: TestClamp (0.00s)
numbers_test.go:15: Clamp(1500, 100, 1000) = 1500, want 1000
numbers_test.go:18: Clamp(2.5, 3, 4) = 2.5, want 3
numbers_test.go:21: Clamp("MAS", "NDLS", "SBC") = "MAS", want "NDLS"
--- FAIL: TestSumBy (0.00s)
numbers_test.go:28: total of the cart = 0, want 395
--- FAIL: TestMedian (0.00s)
numbers_test.go:38: Median([1200 99 545]) = 0, false, want 545, true
numbers_test.go:44: Median of 4, 1, 3, 2 = 0, false, want 2.5, true
FAIL
FAIL example.com/practice 5.361s
FAIL
[exit status 1]
This output changes from run to run: go test prints how long each test and the whole run took, which differs from one run to the next.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py
A first hint for Median: sort a copy made with slices.Clone, because the test checks that the input is unchanged. Then the self-check, one question at a time:
Practice that adapts to you
Practise this lesson
8 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does the ~ in a Go constraint such as ~int mean?
~int stands for every type whose underlying type is int: int itself and every type declared as type X int, such
as type Rupees int. Without the tilde, a constraint term names one exact type, so int | float64 refuses Rupees,
and the compiler suggests the missing ~.
Three longer questions of the module bank build on this lesson:
-
Mid to senior
RoundToRupee[T Number](p T) T, with Number being ~int | ~int64 | ~float64, fails with operator % not defined on p. A teammate proposes a type switch on any(p) inside the function, with math.Mod for the float case. What do you recommend instead, and why?
-
Mid to senior
A generic Max[T cmp.Ordered](xs []T) T over sensor readings returns different maxima for the same readings in a different order, and only in production, where some readings are NaN. Diagnose it, fix it, and say how a test would have caught it.
Key takeaways
- Constraints are interfaces, and their type sets list the type arguments they accept; a union such as
int | float64names exact types, and~intadmits every type whose underlying type is int. - An operator compiles on a type parameter only if every type in its type set supports it, so
%fails under a set with float64 in it and<fails under comparable. - cmp.Ordered, from Go 1.21, is the constraint for
<and the other orderings over integers, floats and strings.
Terms in this lesson
- cmp.Ordered
- The constraint of package cmp, added in Go 1.21, whose type set holds every type with an integer, floating-point or string underlying type, so code may use <, <=, > and >= on its values.
- type set
- The set of types that satisfy an interface. For a constraint such as ~int | ~float64 it is every type whose underlying type is int or float64, and a type argument compiles only if it is in the set.
- underlying type
- The type a defined type is built on: for type Rupees int it is int, and for type Fares []Paise it is []Paise. A ~T term in a constraint matches every type whose underlying type is T.
- union
- A constraint term of the form A | B | C, whose type set holds every type that matches one of its terms, such as int | float64 or ~int | ~int64.
References
- The Go Programming Language Specification: Interface types (The Go Authors)
- The Go Programming Language Specification: Type constraints (The Go Authors)
- The Go Programming Language Specification: Type parameter declarations (The Go Authors)
- Package cmp (The Go Authors)
- Package slices: Max, Sort and SortFunc (The Go Authors)
- An Introduction To Generics (The Go Authors)
- Deconstructing Type Parameters (The Go Authors)
- All your comparable types (The Go Authors)
- Goodbye core types - Hello Go as we know and love it! (The Go Authors)
- Go 1.21 Release Notes: cmp, slices and maps (The Go Authors)
- IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019) (IEEE)
Report a problem with this lesson
Kept only in this browser. Your Learn progress