comparable constraint

The comparable constraint limits generic type parameters to types that support equality comparison operators like == and !=.

The wall of ==

You are writing a utility function to check if a value exists in a slice. You want it to work for strings, integers, and your custom structs. You write func Contains[T any](slice []T, item T) bool and add if v == item. The compiler rejects the code with invalid operation: v == item (operator == not defined on T). You thought any meant "anything," but the compiler disagrees. You need a way to tell Go, "I don't care what the type is, as long as I can compare it with ==." That is the comparable constraint.

What makes a type comparable

Go has a strict list of types that support equality comparison. All basic types like int, string, bool, and float are comparable. Pointers are comparable. Arrays are comparable. Channels are comparable. Structs are comparable if every field inside the struct is comparable.

Slices, maps, and functions are not comparable. Two slices with identical content are not equal unless they reference the same underlying array. Maps are compared by identity, not content. Functions are never comparable. This design keeps comparisons fast and predictable. The language avoids hidden costs and ambiguity.

When you use comparable in a generic, you restrict the type parameter to this safe list. The compiler checks the constraint before generating code. If you try to pass a slice or map, the build fails. At runtime, there is no overhead. The constraint is a compile-time gate.

The bouncer analogy

Think of the == operator as a VIP room. Not every type gets in. The comparable constraint is the bouncer checking IDs at the door. If your type is an int or a string, the bouncer waves you through. If your type is a []byte or a map[string]int, the bouncer stops you. You cannot use == inside the room, so you cannot bring non-comparable types into a function that requires comparable.

This analogy helps explain why any is not enough. any is like an open door. Anyone can enter. But once inside, you might try to use == and get stuck. comparable ensures that everyone who enters is allowed to use the comparison operator.

Minimal example

The Contains function demonstrates the constraint in action. The type parameter T is bound to comparable. This allows the loop to use == safely.

// Contains checks if item exists in slice.
// T must be comparable so we can use ==.
func Contains[T comparable](slice []T, item T) bool {
    // Loop checks each element against the target.
    for _, v := range slice {
        // This line requires T to be comparable.
        // The compiler verifies this constraint.
        if v == item {
            return true
        }
    }
    return false
}

The function works for Contains([]string{}, "hello") and Contains([]int{}, 42). It also works for structs, provided the struct fields are comparable. The compiler generates a specialized version of the function for each type you use. The constraint disappears after compilation.

How the compiler enforces the contract

When you write [T comparable], you are making a contract with the compiler. The compiler checks this contract at every call site. If you call Contains([]map[string]int{}, m), the compiler checks if map[string]int is comparable. It is not. The build fails with invalid operation: v == item (operator == not defined on T).

This error saves you from runtime panics. In languages without generics or with weaker type systems, you might pass a non-comparable type and crash later when the comparison happens. Go catches the mistake early. The error message points directly to the problem. You fix the type or change the function signature.

The constraint also applies to map keys. If you write a generic function that creates a map using the type parameter as the key, you must use comparable. Map keys must be comparable by definition. The compiler enforces this rule. If you try to use any and create a map, you get invalid map key type T. The comparable constraint solves this by guaranteeing the type can be a key.

Realistic example: deduplication

A common use case for comparable is removing duplicates from a slice. You need to track which items you have seen. A map is the perfect tool, but the map key must be comparable. The Unique function uses comparable to enable this pattern.

// Unique returns a slice with duplicates removed.
// Order is preserved based on first occurrence.
func Unique[T comparable](slice []T) []T {
    // Map keys require comparable types.
    // struct{} uses zero memory for values.
    seen := make(map[T]struct{})
    var result []T

    for _, v := range slice {
        // Check if we have seen this value.
        if _, ok := seen[v]; !ok {
            // Mark as seen and add to result.
            seen[v] = struct{}{}
            result = append(result, v)
        }
    }
    return result
}

The function uses map[T]struct{} to track seen values. The struct{} type is empty and takes no memory. This is a standard Go idiom for sets. The comparable constraint ensures T can be a map key. The function preserves the order of first occurrence. It works for any comparable type.

Pitfalls and compiler errors

Slices are never comparable. This catches many developers off guard. You cannot use == to compare two slices. You cannot use a slice as a map key. If you have a struct with a slice field, the struct is not comparable. The compiler rejects the code with struct field slice is not comparable.

If you need to compare slices, you must use reflect.DeepEqual. This function walks the data structure at runtime and compares values recursively. It works, but it is slow. It allocates memory. It bypasses the type system. Use reflect.DeepEqual only when you have no other choice. Prefer restructuring your data to use comparable types whenever possible. For example, use a fixed-size array instead of a slice if the size is known. Arrays are comparable.

Maps and functions have the same restrictions. You cannot compare them with ==. You cannot use them as map keys. The compiler enforces these rules consistently. If you try to use a map as a key, you get invalid map key type map. If you try to compare functions, you get invalid operation: f == g (operator == not defined on func).

Another pitfall is forgetting that comparable is a predeclared identifier. It is not an interface you define. It is a special keyword in the type constraint syntax. You cannot add methods to comparable. You cannot embed it in a custom interface. It represents the set of all comparable types. If you need additional behavior, you must combine comparable with a custom interface using the union syntax. For example, [T interface{ comparable; String() string }] requires the type to be comparable and have a String method.

Convention aside: The community accepts the verbosity of generic constraints. Writing [T comparable] is more explicit than [T any]. The extra characters make the requirements clear. Readers know immediately that the function relies on equality comparison. This clarity is worth the typing.

Decision matrix

Use comparable when you need to use == or != on the type parameter, or when the type must be usable as a map key. Use any when you don't need to compare values and just want to store or pass them through, like in a generic logger or a queue. Use a custom interface like constraints.Ordered when you need comparison operators like < or > for sorting, since comparable only covers equality. Use a specific type instead of generics when the logic is tightly coupled to one type and adding a generic parameter adds no value.

Pick the tightest constraint that lets your code compile. comparable is stricter than any, and that is a feature. Stricter constraints catch errors earlier and make the code easier to understand.

Comparable is a contract, not a runtime check. Trust the compiler to enforce it.

Where to go next