Comparing Simple Comparison Functions in Golang — It is Better to Implement it Yourself!

Kurnianto Trilaksono
4 min readOct 19, 2019

--

Photo by Isaac Smith on Unsplash

I was pulling another all-nighter, thinking about how my co-worker told me about a simple yet a tricky question for company interview. Which kind of like:

“Given two objects, if I’m about to compare it with the comparison operator (==) what would the comparison return?”

“Trrrr— ue… I guess? If you do the strict type check.” I answered. Turns out it’s not the case, the comparison would always return false no matter how strictly you check the type. In addition to my previous answer, there’s function that ease this kind of operation in some programming languages (e.g. equals(), JSON.stringify(), etc). However, my co-worker gave solution to simply change the comparison operand from the object as a whole to all of its attributes, he also doubt the use of reflection as it would cost more.

Hey, I cannot accept that, you cannot change the input for the sake of the solution, so I went home, with this matter in mind.

The Preparation

At home, I created a piece of code in order to do comparison on 4 of popular Golang compare functions. Which are:

  • The good ol’ Golang comparison operator (==)
  • cmp.Equal() function from github.com/google/go-cmp/cmp
  • reflect.DeepEqual() function
  • self implemented isEqual() function

For this experiment, I’m using simple struct definition.

type Person struct {
Name string
Age int
}

Then, I added “constructor” function for Person struct.

func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}

And now let’s create our victims.

func main() {

Adam := NewPerson("Adam", 18)
Adam2 := NewPerson("Adam", 18)
}

Alright, now we’re good to go, let’s start comparing the functions.

The Experiment

  1. Firstly, I measured the Golang comparison operator (==),
// Compare Adam and Adam2, if Adam is the same as Adam2 
// return "awesome!!", else return "uncool".
func main() {

Adam := NewPerson("Adam", 18)
Adam2 := NewPerson("Adam", 18)

var start time.Time
var elapsed time.Duration

// Measuring time for '=='
start = time.Now()
if Adam == Adam2{
fmt.Printf("awesome!!\n")
} else {
fmt.Printf("uncool\n")
}
elapsed = time.Since(start)
log.Printf("'==' took %s \n\n", elapsed)
}

2. Next, I timed the second function which is cmp.Equal() function from go-cmp library.

// Compare Adam and Adam2, if Adam is the same as Adam2 
// return "awesome!!", else return "uncool".
func main() {

Adam := NewPerson("Adam", 18)
Adam2 := NewPerson("Adam", 18)

var start time.Time
var elapsed time.Duration

// Measuring time for cmp.Equal
start = time.Now()
if cmp.Equal(Adam,Adam2) {
fmt.Printf("awesome!!\n")
} else {
fmt.Printf("uncool\n")
}
elapsed = time.Since(start)
log.Printf("cmp.Equal took %s \n\n", elapsed)
}

3. After that, I measured the reflect.DeepEqual() function. DeepEqual inspects the struct attribute and check its values to give conclusion about both compared objects.

func main() {

Adam := NewPerson("Adam", 18)
Adam2 := NewPerson("Adam", 18)

var start time.Time
var elapsed time.Duration

// Measuring time for DeepEqual
start = time.Now()
if reflect.DeepEqual(Adam, Adam2){
fmt.Printf("awesome!!\n")
} else {
fmt.Printf("uncool\n")
}
elapsed = time.Since(start)
log.Printf("DeepEqual took %s \n\n", elapsed)
}

4. Finally, I tried to improvise by creating a simple comparison function which I called “isEqual()”. It asks for 2 objects as the input, checks their types, and then traversing while comparing the values of each attributes using Golang comparison operator.

func isEqual(A, B interface{}) bool {

// Find out the type of A & B is Person or not
if _, ok := A.(*Person); ok {
if _, ok := B.(*Person); ok {
if A.(*Person).Name == B.(*Person).Name {
return A.(*Person).Age == B.(*Person).Age
} else {
return false
}
}
return false
}
return false
}

The Result

The experiment was done by running go run main.go three times, with 5 seconds gap each run.

First try:

awesome!!
2019/10/19 17:24:14 cmp.Equal took 59.934µs
uncool
2019/10/19 17:24:14 '==' took 5.197µs
awesome!!
2019/10/19 17:24:14 DeepEqual took 11.703µs
awesome!!
2019/10/19 17:24:14 isEqual took 3.038µs

Second try:

awesome!!
2019/10/19 17:24:15 cmp.Equal took 57.944µs
uncool
2019/10/19 17:24:15 '==' took 3.423µs
awesome!!
2019/10/19 17:24:15 DeepEqual took 18.238µs
awesome!!
2019/10/19 17:24:15 isEqual took 1.752µs

Third try:

awesome!!
2019/10/19 17:24:15 cmp.Equal took 57.944µs
uncool
2019/10/19 17:24:15 '==' took 3.423µs
awesome!!
2019/10/19 17:24:15 DeepEqual took 18.238µs
awesome!!
2019/10/19 17:24:15 isEqual took 1.752µs

So here, we can see that the fastest function which able to compare the objects is our self implemented comparison function which specifically tailored for Person object, meanwhile the second fastest is DeepEqual() and followed by cmp.Equal(). And also, we can see that the logical comparison operator (==) cannot do a direct comparison of two objects. However, we can use the comparison operator for our self implemented function which surprisingly, 2x faster than using the logical operator by itself.

Implementing our own compare function as we create the struct definition for our object indeed improves our code performance.

So yeah, in fact, what my co-worker said was right about the usage of comparison operator and the reflection, but the solution he proposed can be done better by self implementing the compare function. It is faster, and you don’t need to change the input.

tl;dr — Golang logical comparison operator cannot compare objects directly, but we can use it if you want to implement the compare function yourself which result the fastest comparison function you can have. Also, you should avoid using reflection as much as possible.

** Disclaimer: this article hasn’t been proof read because I don’t have an editor to do it. 😔

--

--