Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A constraint's type set lists the types it accepts · An operator compiles on T only if every type in the set has it. With a pass: 2 in-depth parts and 2 interview answers, about 9 minutes.

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: Rupees against int | float64 Go · missing_tilde.go
// 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: Sum under ~int | ~int64 | ~float64 Go · sum_rupees.go
// 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:

Interactive · step by step Rupees, Paise and float64 against two type sets Step through the checks the compiler makes. A type argument compiles only if it is in the constraint's type set.
Rupees, Paise and float64 against two type sets, step 5 of 5. Showing Rupees (Sum gave 350, a Rupees), int | float64 (refuses Rupees), Paise (Sum gave 64400), Number, float64 (Sum gave 3.75). In focus: Paise, float64, p-number, f-number.not in the setin the setin the setin the setRupeesSum gave 350, a Rupeesint | float64refuses RupeesPaiseSum gave 64400Number~int | ~int64 | ~float64float64Sum gave 3.75

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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 ~.
  3. 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.
  4. 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.
  5. 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 ~

  1. Question 1 of 2 type Rupees int. Which constraint lets Sum([]Rupees{100, 250}) compile?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ~int | ~float64

    A tilde term takes in every type with that underlying type; a plain term takes in one exact type.

  2. Question 2 of 2 Number is ~int | ~int64 | ~float64. Which of these are in its type set?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • float64
    • Paise, declared as type Paise int64
    • int

    The type set of a union is every type that matches one of its terms; a ~T term matches by underlying type.

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: % under Number, and < under comparable Go · operators.go
// 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: the largest item by an ordered key Go · max_by.go
// 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 ~[]E names a slice type and its element type apart, so a function returns the caller’s own slice type.
  • cmp.Compare puts 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

  1. Question 1 of 2 Why does x % 100 not compile in func Paisa[T Number](x T) T, with Number being ~int | ~int64 | ~float64?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: % is not defined on floating-point values, and float64 is in the type set

    The body is checked once for the whole type set, so it may use only what every type in the set supports.

  2. Question 2 of 2 A function returns the larger of two values with a < b. Which constraint lets it accept int, float64 and string?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: cmp.Ordered

    cmp.Ordered exists for exactly this: the types that support <, <=, > and >=.

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.

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:

practice/numbers.go and practice/numbers_test.go

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")
	}
}
The starter functions against their tests Python · practice_session.py
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.

  1. Question 1 of 8 What is the type set of a constraint?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: The set of types that satisfy it, and so may be its type arguments

    Every constraint is an interface, and the types that implement it form its type set.

  2. Question 2 of 8 What does the term ~int64 stand for in a constraint?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: Every type whose underlying type is int64

    ~T is the set of all types whose underlying type is T.

  3. Question 3 of 8 Sum[T Number](xs []T) T is called with []Rupees{100, 250}. What does fmt.Printf("%T", ...) print for its result?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: main.Rupees

    A tilde constraint admits named types and keeps them: the caller gets its own type back.

  4. Question 4 of 8 A money package has type Rupees int and type Paise int64, and will later add type Dollars int64. Which constraint lets Sum accept all three without being edited again?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: ~int | ~int64

    A ~T term is defined by an underlying type, so it admits named types that are declared later; exact terms list types one by one.

  5. Question 5 of 8 When does an operator such as + compile on a value of type T?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: When every type in T's type set supports it

    The type set is the contract: an operator must be defined for each type in it.

  6. Question 6 of 8 Does func Add[T cmp.Ordered](a, b T) T { return a + b } compile?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: Yes: every type in cmp.Ordered has +, and for strings it joins them

    What a constraint allows follows from its types, not from its name: cmp.Ordered allows + but not -.

  7. Question 7 of 8 A function constrained by comparable must find the larger of two values. What should you change?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: Constrain T by cmp.Ordered, which allows <

    Ordering needs a constraint whose types are all ordered, which is what cmp.Ordered lists.

  8. Question 8 of 8 You need x % 100 in a function over money types declared on int and int64, and floats never reach it. Which constraint fits?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: ~int | ~int64

    Make the type set as small as the operations need: % needs integer types only.

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:

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Constraints are interfaces, and their type sets list the type arguments they accept; a union such as int | float64 names exact types, and ~int admits 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.