Explain type constraints — comparable, any, and custom constraint interfaces.

5 minadvancedgogenericsconstraintscomparable

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.

Related Resources