What Is the comparable Constraint in Go

The comparable constraint restricts generic type parameters to types that support equality comparison operators.

When generics meet equality

You are building a utility library. You want a single Contains function that works for integers, strings, and custom structs. You write the loop, use ==, and try to compile. The build fails. The compiler refuses to let you compare everything. Go protects you from comparing things that shouldn't be compared, but it forces you to be explicit about what you allow.

The error points to your generic definition. The compiler tells you that the type parameter does not support equality. You need a way to restrict the generic type to values that can actually be compared. That is what the comparable constraint does.

What comparable actually means

The comparable constraint is a predeclared interface that restricts generic type parameters to types that support equality comparison using == or !=. It acts as a filter. When you write [T comparable], you are telling the compiler that T must be a type where the equality operators make sense.

Think of comparable as a bouncer at a club. The club is the == operator. The bouncer checks the type's ID. If the type has the right structure, it gets in. If the type is a slice, map, or function, the bouncer turns it away. Slices and maps are excluded because comparing them with == is ambiguous and often misleading. Go forces you to write explicit comparison logic for complex data structures instead of hiding bugs behind a simple operator.

You cannot implement comparable manually. Unlike other interfaces, you cannot satisfy comparable by adding methods to a type. The compiler decides based on the type structure. If a type contains a slice field, no amount of Equal methods will make it comparable for ==. This design prevents accidental performance traps and ensures that == always has predictable semantics.

The rules of comparison

The constraint covers a specific set of types. Booleans, numbers, strings, pointers, channels, and arrays are comparable. Structs are comparable only if all their fields are comparable. This rule applies recursively. A struct containing another struct is comparable if the inner struct is also comparable.

Slices, maps, and functions are never comparable. This is a hard rule. Comparing two slices with == would compare their headers: the pointer to the underlying array, the length, and the capacity. That comparison tells you if the slices are the exact same header, not if they contain the same elements. That behavior is rarely what developers want, so Go removes the option entirely.

Pointers are comparable, but they compare addresses, not values. Two pointers are equal only if they point to the same memory location. This is useful for identity checks but dangerous if you expect value equality. The constraint allows pointers, but the semantics require care.

Floating point types satisfy comparable, but NaN values break equality logic. math.NaN() == math.NaN() evaluates to false. The constraint allows the operation, but the result might surprise you. Floating point comparison is fast, but it follows IEEE 754 rules where NaN is not equal to anything, including itself.

Minimal example

Here is the simplest generic function that needs comparison. It checks if a slice contains a specific item.

// Contains checks if item exists in slice.
func Contains[T comparable](slice []T, item T) bool {
	// T must be comparable for the == operator to work.
	// The compiler verifies this constraint at the call site.
	for _, v := range slice {
		if v == item {
			return true
		}
	}
	return false
}

The constraint [T comparable] sits right after the type parameter name. It restricts what types can be passed. If you call Contains([]int{1, 2}, 2), the compiler sees that int is comparable and generates the code. If you call Contains([][]int{{1}}, []int{1}), the compiler sees that []int is not comparable and rejects the call.

The error occurs at the call site, not the definition. This keeps the generic function reusable while catching mistakes where they happen. The compiler rejects the program with invalid operation: v == item (slice can only be compared to nil) if you pass a slice type. The message tells you exactly why the comparison failed.

Walkthrough

When you compile, the compiler performs two checks. First, it verifies that the constraint is satisfied by the concrete type. Second, it generates the function body with the concrete type substituted in.

If the constraint is satisfied, the generated code uses the native == operator for that type. For integers, this is a single CPU instruction. For strings, this compares length and then memory contents efficiently. For structs, this compares each field recursively. The performance is optimal because the compiler knows the exact type.

If the constraint is not satisfied, the compiler stops. It does not generate code. It does not fall back to reflection. It does not try to find an Equal method. The constraint is a hard requirement. This guarantees that your generic code never pays a runtime penalty for type checking. The cost is paid once at compile time, and the result is fast, type-safe code.

Realistic example

Here is a function that removes duplicates from a slice. It uses a map to track seen items. Map keys must be comparable, so the constraint is essential.

// Unique returns a slice with duplicates removed.
func Unique[T comparable](items []T) []T {
	// Use a map for O(1) lookups.
	// The map key type must be comparable.
	seen := make(map[T]struct{})
	var result []T
	for _, item := range items {
		// Check if we've seen this item before.
		// The underscore discards the value intentionally.
		if _, exists := seen[item]; !exists {
			seen[item] = struct{}{}
			result = append(result, item)
		}
	}
	return result
}

The underscore in _, exists follows Go convention. It discards the map value intentionally. It signals to readers that you checked the second return value and chose to ignore it. The empty struct struct{}{} is used as the map value to minimize memory usage. This is a common pattern in Go for sets.

The function works for any comparable type. You can pass []string, []int, or []MyStruct as long as MyStruct has only comparable fields. The compiler generates a specialized version of the function for each type. The map operations use the native hashing and comparison logic for the type.

Pitfalls and errors

Structs with non-comparable fields are a common trap. If a struct contains a slice, map, or function, the struct itself is not comparable. The compiler catches this with an error like invalid operation: cannot compare a == b (struct containing []int cannot be compared). You cannot use such a struct as a map key or in a generic function with [T comparable].

To fix this, you have two options. Remove the non-comparable field if it is not needed. Or change the design to use a custom comparison method. If you need equality logic for a struct with slices, define an Equal(other T) bool method and use a custom interface constraint instead of comparable.

Floating point NaN values are another pitfall. The constraint allows float64, but NaN breaks equality. If your data might contain NaN, the Contains function will return false even if NaN is present. This is correct behavior for ==, but it might not match your business logic. You need to handle NaN explicitly if it matters.

Forgetting the constraint is a frequent mistake. If you write [T any] and try to use ==, the compiler rejects it with invalid operation: v == item (operator == not defined on T). You must add comparable to the constraint. The error message is clear, but it forces you to revisit the generic definition.

Decision matrix

Use [T comparable] when your generic function uses ==, !=, or passes the type as a map key.

Use [T any] when you only need to store, move, or iterate over values without comparing them.

Use a custom interface with an Equal(other T) bool method when the type contains non-comparable fields like slices, but you still need equality logic.

Use a custom interface with a Hash() int method when you need map keys for types that aren't comparable.

Use reflection only when the type is unknown at compile time and you cannot define a constraint.

Where to go next

Constraints are promises. Make them as tight as possible. The compiler will thank you.