Explain type constraints — comparable, any, and custom constraint interfaces.
Quick Answer
A type constraint limits which types can be used for a generic type parameter, written as an interface listing allowed types or required methods. any (an alias for interface{}) is the loosest constraint — it allows literally any type. comparable allows only types that support ==/!=, which is required for things like using a type as a map key. A custom constraint interface can list specific types with a union (int | int64 | float64), require specific methods like a normal interface, or combine both. The compiler enforces the constraint at every call site, rejecting a type argument that doesn't satisfy it, which is what makes generics fully type-safe rather than a thin wrapper around any.
Detailed Answer
Constraints are what keep generics type-safe. Without them, a type parameter could be anything, and you couldn't do much with it.
any: no constraint at all
func Print[T any](v T) {
fmt.Println(v) // works for anything, but you can't do arithmetic, compare, etc.
}
comparable: required for map keys and ==
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target { // requires comparable — `any` alone wouldn't allow ==
return true
}
}
return false
}
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "c")) // false
Custom union constraints
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
The ~ before a type means "this type, or any type whose underlying type is this" — so a custom type like type Age int still satisfies ~int, even though Age and int are technically different named types.
Constraints requiring methods
type Stringer interface {
String() string
}
func Join[T Stringer](items []T, sep string) string {
parts := make([]string, len(items))
for i, v := range items {
parts[i] = v.String()
}
return strings.Join(parts, sep)
}
This works exactly like a normal interface constraint, requiring any type argument to implement String() string.
Why the compiler enforcing this matters
Calling Max("a", 5) with mismatched types, or Contains([]MyStruct{...}, target) where MyStruct isn't comparable, fails to compile immediately. This is exactly the safety generics were built to add over the old any-based approach, where the same mistake would only surface as a runtime panic.