Engineering Blog

Comparing values incorrectly

In software development comparing values between fields is a common operation. Writing a function to compare two objects, and testing to compare a value to the expected result are some of the frequently implemented comparisons. While comparing our first insight might be to use the == operator everywhere. But this should not always be the case. As we will discuss in which case the == operator should be used. Let’s start with a concrete example. We create a basic product struct and use == to compare two instances.

type product struct {
id string
}
func main() {
prod1 := product{id: "x"}
prod2 := product{id: "x"}
fmt.Println(prod1 == prod2)
}

Comparing these two customer structs is a valid operation in Go, and it will print true . Now, what happens if we make a slight modification to the customer struct toadd a slice field?

type product struct {
id
string
supply []float64
}
New field
func main() {
prod1 := product{id: "x", supply: []float64{1.}}
prod2 := product{id: "x", supply: []float64{1.}}
fmt.Println(prod1 == prod2)

We might expect this code to print true as well. However, it doesn’t even compile:
invalid operation:
prod1 == prod2 (struct containing []float64 cannot be compared)
The problem relates to how the == and != operators work. These operators don’t work
with slices or maps. Hence, because the customer struct contains a slice, it doesn’t compile. It’s essential to understand how to use == and != to make comparisons effectively. We can use these operators on operands that are comparable.

There might be an issue while using the == operator with any type. For example two any type values are compared:-

var a any = 3
var b any = 3
fmt.Println(a == b)

This code output is true

But what if we initialize two product types and assign them to any types

var prod1 any = products{id: "x", operations: []float64{1.}}
var prod2 any = products{id: "x", operations: []float64{1.}}
fmt.Println(prod1 == prod2)

This code compiles. But as both types can’t be compared because the customer struct
contains a slice field, it leads to an error at run time.

To compare two slices, two maps, or two structs containing noncomparable types we can use run-time reflection with the reflect package. For example, in Go, we can use reflect.DeepEqual. This function reports whether two elements are deeply equal by recursively traversing two values. The elements it accepts are basic types plus arrays, structs, slices, maps, pointers, interfaces, and functions.

var prod1 any = products{id: "x", operations: []float64{1.}}
var prod2 any = products{id: "x", operations: []float64{1.}}
fmt.Println(reflect.DeepEqual(prod1,prod2)

Even though the customer struct contains noncomparable types (slice), it operates as
expected, printing true.

The other catch is something pretty standard in most languages. Because this function uses reflection, which introspects values at run time to discover how they are formed, it has a performance penalty. Doing a few benchmarks locally with structs of different sizes, on average, reflect.DeepEqual is about 100 times slower than ==. This might be a reason to favor using it in the context of testing instead of at the run time. If performance is a crucial factor, another option might be to implement our own comparison method. Here’s an example that compares two customer structs and returns a Boolean:

func (a product) equal(b product) bool {
if a.id != b.id {
return false
}
if len(a.supply) != len(b.supply) {
return false
}
for i := 0; i < len(a.supply); i++ {
if a.supply[i] != b.supply[i] {
return false
}
}
return true
}

In general, we should remember that the == operator is pretty limited. For example, it doesn’t work with slices and maps. In most cases, using reflect.DeepEqual is a solution, but the main catch is the performance penalty. However, if performance is crucial at run time, implementing our custom method
might be the best solution.

References:-

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
Previous Post
Next Post