Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Golang comparable: The Constraint That Changed Meaning in Go 1.20

Sean

Platform Writer

Aug 27, 2026
7 min read

comparable is a predeclared constraint that permits equality comparison on a type parameter. It does not permit ordering, and since Go 1.20 the set of types that satisfy it is wider than the set of types that are strictly comparable.

Golang comparable: The Constraint That Changed Meaning in Go 1.20

Two things confuse people about comparable. The first is that it sounds like it should allow ordering, and it does not. The second is that its meaning genuinely changed in Go 1.20, so older answers describe a rule that no longer holds.

Table of contents

What comparable permits

A type parameter constrained by comparable can be used with the equality and inequality operators, and can be used as a map key. That is the whole contract.

func Index[T comparable](haystack []T, needle T) int {
    for i, v := range haystack {
        if v == needle {
            return i
        }
    }
    return -1
}

func Unique[T comparable](items []T) []T {
    seen := make(map[T]struct{}, len(items))
    out := make([]T, 0, len(items))
    for _, i := range items {
        if _, ok := seen[i]; ok {
            continue
        }
        seen[i] = struct{}{}
        out = append(out, i)
    }
    return out
}

Both of these are impossible without the constraint. A bare any type parameter cannot be compared for equality at all, because any includes types like slices where equality is not defined.

The types that satisfy comparable are the ones usable as map keys: booleans, numerics, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable, and - with a caveat below - interfaces.

What it does not permit

comparable says nothing about ordering. This does not compile:

// Does NOT compile
func Max[T comparable](a, b T) T {
    if a > b { // invalid: type parameter T is not ordered
        return a
    }
    return b
}

The reason is straightforward once stated: pointers, channels, and structs are comparable for equality but have no defined ordering. Allowing the greater-than operator on comparable would mean allowing it on types where it means nothing.

For ordering, the constraint is cmp.Ordered from the standard cmp package, added in Go 1.21.

import "cmp"

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

cmp.Ordered covers integers, floats, and strings - everything with a defined less-than. Before 1.21, the same constraint lived in the experimental constraints package, and a lot of code still imports it from there. If you are on 1.21 or later, move to cmp and drop the dependency.

Note also that Go 1.21 added min and max as builtins, so the function above is now mostly a teaching example rather than something you need to write.

The Go 1.20 change

Before Go 1.20, comparable was satisfied only by types that are strictly comparable - types where equality can never panic at runtime. That excluded interfaces, because comparing two interfaces holding non-comparable dynamic types panics.

var a any = []int{1, 2}
var b any = []int{1, 2}
fmt.Println(a == b) // panic: comparing uncomparable type []int

The old rule meant a map keyed by any was legal - ordinary maps have always accepted interface keys - while a generic function constrained by comparable refused to accept any as a type argument. Same operation, two different answers, for no reason a user could act on.

Go 1.20 changed the rule: comparable is now satisfied by all comparable types, including interfaces, and the possibility of a runtime panic is accepted as the cost. In exchange, generic code composes with ordinary map code.

// Legal since Go 1.20
func Count[T comparable](items []T) map[T]int {
    m := make(map[T]int)
    for _, i := range items {
        m[i]++
    }
    return m
}

vals := []any{1, "a", 1, true}
fmt.Println(Count(vals)) // map[1:2 a:1 true:1]

The practical consequence: if you instantiate a comparable generic with an interface type, comparison can panic at runtime, exactly as it can with a plain map. That is the same risk profile as before generics existed, which is the point.

Structs, arrays, and what breaks

A struct is comparable if every field is. This is the rule that determines whether your domain types can be map keys or set elements, and it is easy to break by accident.

type Key struct {
    Region string
    Shard  int
}
// Comparable. Usable as a map key, satisfies comparable.

type Bad struct {
    Region string
    Tags   []string // slices are not comparable
}
// Not comparable. Compile error as a map key.

Adding one slice field to a struct that is used as a map key somewhere else in the codebase produces a compile error at the usage site, not the definition. The error can be a long way from the change that caused it.

Arrays are comparable if the element type is, so a fixed-size array of strings works as a key while a slice of strings does not. That is occasionally a useful escape hatch: converting a bounded slice to an array makes it usable as a key.

Two more subtleties worth knowing. Struct equality compares all fields including unexported ones, so a struct with an internal cache field will compare unequal to an otherwise-identical value. And floating-point fields inherit NaN semantics - a struct containing a NaN is never equal to itself, which quietly breaks map lookups.

Choosing a constraint

The decision is short. Ask what operation the function body needs.

  • Needs equality or a map key: comparable.
  • Needs ordering or sorting: cmp.Ordered.
  • Needs neither, just holds values: any.
  • Needs a method: an interface constraint declaring that method.
  • Needs arithmetic across numeric types: a union constraint listing the underlying types.

Start with the weakest constraint the body actually requires. Over-constraining a helper to cmp.Ordered when it only compares for equality means callers cannot use it with their struct types, for no benefit.

The tilde prefix matters for union constraints: it means any type whose underlying type matches, so a named type declared over an int satisfies it. Without the tilde, only the exact predeclared type does, and every domain-specific named type in your codebase is locked out.

How this fits the rest of the stack

Generic helpers are the kind of code that lives in an internal package and gets touched by every service you run. Push the repo, get a build log, and see the failing compile in the same place as the failing request. The RunxBuild hosting calculator puts the Go service, the managed Postgres, and the bandwidth on one page so the cost of running the thing is as legible as the build output.

Useful related references:

FAQ

What is the difference between comparable and cmp.Ordered?

comparable permits equality comparison and allows use as a map key. cmp.Ordered permits the relational operators, and covers only integers, floats, and strings. Pointers and structs satisfy comparable but not cmp.Ordered, because they have no defined ordering.

Why can’t I use the greater-than operator with a comparable type parameter?

Because comparable includes types with no ordering - pointers, channels, and structs can be tested for equality but not sorted. Allowing relational operators would mean allowing them on types where they are meaningless. Use cmp.Ordered when the function body needs ordering.

What changed about comparable in Go 1.20?

Before 1.20, comparable required strict comparability, which excluded interface types even though ordinary maps accept them as keys. Go 1.20 widened it so all comparable types satisfy it, including interfaces. The trade-off is that comparison can now panic at runtime if the dynamic type is not comparable - the same behaviour ordinary maps have always had.

Can a struct satisfy comparable?

Yes, provided every field is comparable. Structs of strings, numbers, booleans, pointers, and arrays of comparable types work. A struct containing a slice, map, or function does not, and using it as a map key is a compile error at the usage site rather than the definition.

Should I use constraints.Ordered or cmp.Ordered?

Use cmp.Ordered from the standard library if you are on Go 1.21 or later. The constraints package lives in the experimental module, which carries no compatibility promise. They are functionally equivalent, so the migration is a one-line import change.

#golang comparable#go generics#cmp.Ordered#type constraints#go 1.20