The comparable Constraint
Learn how Go's predeclared comparable interface restricts type parameters to types that support equality operators, the difference between strict and run-time comparability, and practical patterns for building generic code with maps, sets, and search functions.
When you write a generic function in Go, you often need to compare two values for equality. The comparable interface is the language mechanism that tells the compiler: "I need to use == and != on values of this type parameter, so only allow types that support those operators."
It is one of the few predeclared identifiers that can appear as a type constraint. Unlike most interfaces, comparable cannot be used to declare a variable; it exists solely to constrain type parameters.
What comparable Guarantees
A type parameter constrained by comparable promises that two values of that type can be compared with == and !=. This lets you write functions like:
func Equal[T comparable](a, b T) bool {
return a == b
}
Calling Equal(1, 1) or Equal("go", "rust") works because int and string are comparable types. A slice, on the other hand, cannot be compared with == in Go, so trying to call Equal([]int{1}, []int{1}) produces a compile-time error—[]int does not satisfy comparable.
The set of comparable types in Go includes:
- numeric types, string, bool
- pointers
- channels
- arrays whose element type is comparable
- structs whose fields are all comparable
- interfaces (with a runtime caveat)
Strict Comparability and the Go 1.20 Change
Not all comparable types are equal in the eyes of the compiler. Some comparisons are safe at compile time and will never panic; these types are called strictly comparable. A type is strictly comparable if it is comparable and does not contain any interface types. For example, int, string, and struct{ X int } are strictly comparable. An interface type like any (which can hold values of any type) is comparable, but it is not strictly comparable because the comparison may panic at runtime if the dynamic value inside the interface is itself not comparable.
Before Go 1.20, the comparable constraint only accepted types that were strictly comparable. That meant you could not use any as a key type in a generic map:
type MyMap[K comparable, V any] map[K]V
var m MyMap[any, string] // compile error before Go 1.20
This was surprising because plain non‑generic maps like map[any]string have always been valid in Go. The restriction made it impossible to write certain generic data structures that mirrored built‑in map behavior.
Go 1.20 relaxed the rule: now a type satisfies comparable if it is comparable by the language specification, even if it is not strictly comparable. The generic map above compiles fine with Go 1.20 and later. However, the safety guarantee changes. With a strictly comparable key type you know that map lookups and insertions will never panic because of the comparison itself. With an interface key type, a panic still lurks at runtime if you store a non‑comparable dynamic value.
Runtime panics are still possible:
Even with Go 1.20, using an interface type that satisfies comparable does not eliminate the possibility of a runtime panic. If you insert a slice or a map into a map keyed by any, the program will panic when the map tries to compare keys. The comparable constraint only ensures the compiler that the type parameter supports == — it does not guard against the dynamic type stored in an interface.
Types That Never Satisfy comparable
Some Go types are fundamentally incomparable. The compiler rejects them outright when they appear as type arguments for a comparable parameter:
// Each of these will fail to compile:
func Bad1[T comparable](a, b T) bool { return a == b }
Bad1[[]int] // slices are not comparable
Bad1[map[string]int] // maps are not comparable
Bad1[func()] // functions are not comparable
type HasSlice struct { Items []string }
Bad1[HasSlice] // struct containing a slice is not comparable
A struct is comparable only when every field is comparable. If one field is a slice, a map, a function, or a struct that itself contains any of those, the entire struct becomes incomparable.
Practical Generic Functions
The real power of comparable shows up when you build reusable functions that work across all kinds of slice elements or map keys.
Contains — checking membership
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
This works for any slice whose element type supports ==. You can use it with []int, []string, or even []Point if Point is a struct with only comparable fields. When the element type is not comparable — say a slice of byte slices — the compiler stops you before the program runs.
Unique — removing duplicates
func Unique[T comparable](items []T) []T {
seen := make(map[T]struct{})
result := make([]T, 0, len(items))
for _, item := range items {
if _, exists := seen[item]; !exists {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
The map seen uses the type T as a key, which forces T to be comparable. This is a classic pattern: by declaring T comparable, you gain the ability to use maps inside the generic code safely at compile time (for strictly comparable types) and with the runtime caveat for interface types.
FirstDifference — locating a mismatch
func FirstDifference[T comparable](a, b []T) int {
minLen := len(a)
if len(b) < minLen {
minLen = len(b)
}
for i := 0; i < minLen; i++ {
if a[i] != b[i] {
return i
}
}
if len(a) != len(b) {
return minLen
}
return -1
}
Here the inequality operator != is allowed only because T is constrained by comparable. Without that constraint, the compiler would reject the a[i] != b[i] expression.
Comparable does not mean ordered:
The comparable constraint only enables == and !=. If you need < or >, you must use a different constraint — typically the experimental constraints.Ordered interface or a custom union like ~int | ~float64 | ~string.
Generic Data Structures
The most common use of comparable is in data structures that depend on key equality, such as maps and sets.
A generic set type
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T])
for _, item := range items {
s[item] = struct{}{}
}
return s
}
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
func (s Set[T]) Contains(item T) bool {
_, ok := s[item]
return ok
}
Because the underlying map’s key type must be comparable, the Set type parameter must also be comparable. This is enforced at compile time. If you try Set[[]int], the compiler will refuse to compile the NewSet call.
A generic cache
type Cache[K comparable, V any] struct {
data map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{data: make(map[K]V)}
}
func (c *Cache[K, V]) Set(key K, value V) {
c.data[key] = value
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
val, ok := c.data[key]
return val, ok
}
A cache keyed by int or string is safe. A cache keyed by an interface type works, but be mindful of what you put into that interface. This is the trade‑off Go 1.20 introduced: more flexibility with comparable, at the cost of pushing some safety checks from compile time to runtime.
When your keys are strictly comparable:
If your generic code will always be instantiated with basic types, structs of basic types, or pointers to those, you are in the safest territory. No runtime panic can occur, and the compiler keeps you from using a slice or map as a key.
Combining comparable with Other Constraints
You often need more than just equality. Go lets you embed comparable into a larger interface to demand both comparison and a method set.
type ComparableStringer interface {
comparable
fmt.Stringer
}
A type argument for a parameter constrained by ComparableStringer must support == and implement the String() string method. This works for defined types like:
type Key struct {
Namespace string
ID int
}
func (k Key) String() string {
return fmt.Sprintf("%s:%d", k.Namespace, k.ID)
}
Key is strictly comparable because its fields are string and int, so it satisfies both parts of the constraint without any runtime risk. If you tried to satisfy the constraint with an interface type that has String() but is not strictly comparable, the code compiles (Go 1.20+), but a panic might hide in the == operation later.
When to Use comparable vs. any
The decision is straightforward:
- Use
comparablewhen you need to compare values of the type parameter — for map keys, membership tests, deduplication, or any==/!=expression. - Use
any(or a specific interface) when you never compare the values directly.
A function that only stores or passes around values without touching them should use any. A function that looks for a value in a slice or uses it as a map key needs comparable.
// Needs comparable: uses == to find the value
func Index[T comparable](slice []T, target T) int { ... }
// Only stores the value; no comparison needed
func Store[T any](cache map[string]T, key string, val T) {
cache[key] = val
}
comparable is not for custom equality:
If you need case‑insensitive string matching or comparison that considers a tolerance for floating point numbers, comparable will not help. Those situations require a custom equality function passed alongside the generic operation, often called ContainsFunc or EqualFunc.
Key Takeaways
The comparable constraint is the compiler’s way of guaranteeing that you can use == and != inside generic code. It started out as a restriction to strictly comparable types, which prevented legitimate use cases like wrapping map[any]string. Go 1.20 broadened the rule: any type that is comparable by the language spec now satisfies comparable, but the runtime safety net is your responsibility when interfaces are involved.
Understanding the boundary between strict and runtime comparability is the single most important concept when working with this constraint. When your types are built from ints, strings, and structs of those, you get zero‑cost compile‑time protection. When an interface enters the picture, the compiler trusts you to only store values whose dynamic types are comparable.