Skip to Content
Course content

175: Comparable Constraint Usage

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen all the time when developers first dive into Go generics: they see the built-in comparable constraint and assume it's a "catch-all" for any type that can be compared in any way. Specifically, there's a common belief that if a type satisfies comparable, you can use it for sorting or finding the maximum value in a list.

"Comparable means I can use the greater-than operator"

It sounds intuitive, right? If I can compare two things, I should be able to tell which one is "bigger." But in Go, comparable is much narrower than that. It only guarantees that the == and != operators work. It tells absolutely nothing about <, >, <=, or >=.

Let's look at where this falls apart. If you try to write a generic Max function using comparable, the compiler will shut you down immediately:

type Number interface {
    comparable
}

func Max[T Number](a, b T) T {
    if a > b { // ❌ Compiler Error: invalid operation: a > b (type T does not support operator >)
        return a
    }
    return b
}

The compiler is being pedantic here because it has to be. Think about comparable types that aren't numbers—like structs (provided their fields are also comparable) or booleans. While you can check if two structs are identical, it makes no sense to ask if one struct is "greater than" another without a custom definition. Go refuses to guess your intent.

The strict boundary between equality and order

To use comparable correctly, you have to shift your mindset: use it when you are checking for identity, not magnitude. The most common real-world application of comparable is when you're building something that behaves like a map or a set, where you just need to know "is this the same item I saw before?"

I usually reach for comparable when I'm writing utility functions that search through slices or filter out duplicates. Since map keys must be comparable, comparable is the perfect constraint for any generic function that uses a map internally to track occurrences.

Here is a proper way to use it. Let's say we want a function that finds the first index of a specific element in any slice. We don't care if the elements can be sorted; we just care if they are equal:

func FindIndex[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target { // ✅ This is exactly what comparable is for
            return i
        }
    }
    return -1
}

// Usage
names := []string{"Alice", "Bob", "Charlie"}
idx := FindIndex(names, "Bob") // Returns 1

ids := []int{101, 202, 303}
idx2 := FindIndex(ids, 202) // Returns 1

If you actually need to use < or >, you can't use the built-in comparable. You'll need to use the cmp.Ordered constraint from the standard library's cmp package (or define your own interface that covers int, float64, string, etc.). Just remember: comparable is for equality. Period.




📋 Practical Task

Build a Generic Duplicate Element Detector

Your task is to create a generic function called HasDuplicates. This function should take a slice of any type T and return a boolean indicating whether any element appears more than once in the slice.

Requirements:

  • The function must use the comparable constraint to ensure the type T can be used as a map key.
  • Use a map[T]bool (or a map to an empty struct map[T]struct{}) to track the elements you have already encountered while iterating through the slice.
  • The function should return true as soon as the first duplicate is found, and false if the loop completes without finding any duplicates.

Test your implementation with a slice of strings (e.g., []string{"apple", "banana", "apple"}) and a slice of integers (e.g., []int{1, 2, 3, 4}) to ensure the generic constraint is working correctly across different types.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.