skip to content

In Go generics, what is a type constraint, and what can a function do with a value of a type parameter constrained by any?

level: juniorimportance: must knowfreq 72%

answer

  1. an interface used in a new position
  2. it names a set of types, not just methods
  3. the body may use only what every member allows
  4. any admits everything, so it permits almost nothing

basics

~20 s

A constraint is an interface that bounds a type parameter, defining which types may be used as the type argument. The any constraint admits every type, so the body may only assign, pass and store such values, not compare or add them.

solid answer

~50 s

Every type parameter carries a constraint, and a constraint is always an interface. Since generics, an interface describes a **type set**: a method-only interface like `fmt.Stringer` has the type set of all types with that method, and a constraint may also list types directly, as in `interface { int | float64 }`. The rule that follows is the one people miss: an operation is legal on a value of type parameter type `T` only if it is legal for *every* type in the set. So under `[T any]` you can copy the value, pass it on, store it in a slice or return it, but `x + y`, `x < y` and even `x == y` do not compile, because `any`'s type set includes structs, slices and maps. Constraints are checked entirely at compile time when the function is instantiated; nothing is verified at run time.

code

go · 17 lines
go
// A constraint interface whose type set is int and float64.
type Number interface {
	int | float64
}

func TotalScore[T Number](scores []T) T {
	var total T
	for _, s := range scores {
		total += s // legal: + works for every type in the set
	}
	return total
}

func CountEntries[T any](entries []T) int {
	// total += e would not compile: any permits no operators
	return len(entries)
}

go deeper

for a junior

Be ready to say that a constraint is an interface naming the set of permitted types, and that under any you can only move a value around, not compare or add it.

for a middle

Explain that an interface's elements intersect to form a type set, and that an operator is legal on T only when every type in that set supports it with the same meaning.

for a senior

Show judgment about tightness: a constraint that merely restates any adds a type parameter without buying anything, while an over-narrow union locks out callers' defined types.

for a principal

Own the fact that a constraint written into an exported signature is part of the package's compatibility surface: widening it later is usually safe, narrowing it breaks every caller's instantiation.

## What a constraint is A generic declaration in Go lists **type parameters** in square brackets, each with a constraint: ```go func TotalScore[T Number](xs []T) T type Board[K comparable, V any] struct{ ... } ``` `Number`, `comparable` and `any` are the constraints. A constraint is always an interface — generics did not add a new kind of declaration for them. ## Interfaces got a second job: the type set Before generics an interface meant a set of methods, and a value satisfied it when its method set contained them. Generics reinterpreted every interface as defining a **type set** — the set of types that are elements of it. - A method-only interface such as `fmt.Stringer` has as its type set every type with a `String() string` method. Used as a constraint, it admits exactly those types. - A constraint may instead (or additionally) list types directly. `interface { int | float64 }` is a **union** whose type set is those two types. - `~int` is an **approximation term**: every type whose underlying type is `int`, so a caller's `type Score int` is included. - `comparable` is a predeclared constraint whose type set is the types on which `==` is defined and cannot panic. - Elements can be combined; the type set of an interface with several elements is their **intersection**. `any` is the predeclared alias for `interface{}`. As a constraint it places no restriction at all: every type argument is accepted. ## What the body may do with a T This is the practical half of the answer. Inside the generic body, an operation on a value of type parameter type `T` is permitted only when it is permitted for **every** type in `T`'s type set, with the same meaning. That gives a sharp trade: | Constraint | Legal on a T value | |---|---| | `any` | assign, pass, return, store, take the address, use in a slice or map value position | | `comparable` | the above, plus `==` and `!=`, plus use as a map key | | `cmp.Ordered` | the above, plus `<`, `<=`, `>`, `>=` and `+` (all its members support them) | | `interface{ int \| float64 }` | arithmetic operators supported by both members | | `fmt.Stringer` | the method, plus the `any`-level operations | So `[T any]` is the weakest possible bound: it says "I will not look inside this value". A function like `Count[T any](xs []T) int` or `First[T any](xs []T) T` is honest under `any`; a function that needs to add scores together is not, and must ask for a narrower type set. A common beginner surprise is that `x == y` does not compile under `any`. Slices, maps and functions have no `==`, and they are in `any`'s type set, so the compiler refuses the whole operation rather than deciding per instantiation. ## Constraints are a compile-time device When you instantiate a generic function or type, the compiler checks that each type argument satisfies its constraint and reports something of the form "X does not satisfy C" if it does not. There is no run-time check, no reflection, and no per-constraint boxing decision you control. If it compiles, the constraint is honoured for that instantiation. ## A constraint interface is not always a usable type An interface that contains anything other than methods — a union, a `~T` term, or `comparable`, directly or embedded — may be used **only** as a constraint. Declaring `var n Number` where `Number` is `interface{ int | float64 }` is a compile error. A method-only interface has no such restriction and can serve in both roles, which is why `fmt.Stringer` works as a constraint and as a variable type. ## Picking a constraint Start from what the body actually does and pick the weakest constraint that permits it. `any` when you only move values around; `comparable` when you key a map or de-duplicate; `cmp.Ordered` when you compare magnitudes; a union such as `~int64 | ~float64` when you do arithmetic on a caller's numeric types; a method-only interface when you need behaviour rather than representation. Over-tight constraints lock out callers' defined types for no gain; over-loose ones will not compile once the body grows.

  • Can a constraint interface also be used as the type of an ordinary variable?
    Only if it lists nothing but methods. An interface containing a union, a `~T` term or `comparable` — directly or embedded — may be used only as a constraint, and declaring a variable of that type is a compile error. `fmt.Stringer` has no such restriction and works in both positions.
  • What exactly is any, and how does it differ from interface{}?
    `any` is a predeclared alias for `interface{}`, introduced with generics in Go 1.18. It is the same type, not a new kind of thing; the alias exists because `[T any]` reads better than `[T interface{}]`. As a constraint it accepts every type argument.
  • Why does the compiler reject total += x when T is constrained by any?
    An operator is allowed on a type parameter only when every type in its type set supports it. `any`'s type set includes structs, slices, maps and functions, none of which support `+`, so the compiler rejects the expression outright rather than deferring the decision to each instantiation.

A constraint is a guest list, not a job description: it says which types may come in, and the body may only ask for what every guest can do.

saying these in an interview costs you the question

  • Says a constraint is just an interface listing methods
  • Thinks any lets the body use == or +
  • Believes constraints are checked at run time by reflection
  • Expects a union constraint to work as a variable type