Relooking At Golang's reflect.DeepEqual()


By Vibhu Garg

When I was pretty new to Golang, I was developing a Golang client for one of our platform services in our team in Gojek. I discovered a few things which may prove useful for beginners, as well as people who are well-versed with Golang. Read on 👇

Problem Statement

Broadly, this is with respect to data types in Golang. I wanted to compare two objects — essentially two primitives in Golang. Two interfaces are said to be equal if and only if they have the same dynamic type & their values are equal. For an instance, let’s take up one example to understand the problem deeper.

package main
import "fmt"
func main() {
   var x interface{}
   x = 5
   switch x.(type) {
   case int:
      fmt.Printf("given number {%d} is integer", x)
   default:
      fmt.Printf("given number {%v} is other than integer", x)
   }
}

When I run the above code, the output is as below-

$ go run main.go
given number {5} is integer

This is as expected as x is of type interface and the underlying dynamic type of x is an integer.

What’s the problem, then?

I had to compare one of the key-value pairs in the json with one of the stored variables as per the business use-case. Let’s see it.

package main
import (
   "encoding/json"
   "fmt"
   "os"
   "reflect"
)
type School struct {
   Id   interface{}
   Name string
}
func main() {
   x := School{
      Id:   1,
      Name: "Golang Public School",
   }
   bytes, err := json.Marshal(x)
   if err != nil {
      println("error while unmarshalling the json ", err)
      os.Exit(1)
   }
   var y School
   err = json.Unmarshal(bytes, &y)
   if err != nil {
      println("error while unmarshalling the json ", err)
      os.Exit(1)
   }
   if reflect.DeepEqual(x, y) {
      println("both x & y are same")
   } else {
      println("x & y are different")
   }
   fmt.Println("value of x is ", x)
   fmt.Println("value of y is ", y)
}

When I run the above code, the output was a little unexpected for me as a beginner in Golang, but surprisingly it was the same even for other experienced devs also.

$ go run main.go
x & y are different
value of x is  {1 Golang Public School}
value of y is  {1 Golang Public School}

But, when I ran the above code by changing school Idas 1.2, the output was what I expected earlier.

...

func main() {
   x := School{
      Id:   1.2,
      Name: "Golang Public School",
   }
 ...
}

The output was:

$ go run main.go

both x & y are same
value of x is  {1.2 Golang Public School}
value of y is  {1.2 Golang Public School}

This required debugging & understanding what was actually happening in the above case.

Variables as seen in debugging [Fig-1]

After debugging at every step to know what was happening, we observed one thing. If you see Fig-1, you’ll see the dynamic type of y after unmarshalling got changed to float64 from an int . So, whenever we do reflect.DeepEqual() , it gave false as the underlying dynamic type of both x & y were different.

What to do next?

After rummaging through Stack overflow and other articles on google, I was redirected to the Golang documentation itself — the documentation of Unmarshal function.

unmarshalling float64

Our business use case was such that we could not ignore the underlying data type. We tried some hacks like converting the dynamic types to int , if its underlying type isfloat64 and it’s a whole number. This was working fine but it was causing some problems when the interfaces were becoming more & more complex.

Solution

Canonical form: A canonical form is a representation such that every object has a unique representation (with canonicalization being the process through which a representation is put into its canonical form). Thus, the equality of two objects can easily be tested by testing the equality of their canonical forms [Source: Wikipedia].

We thought of writing a wrapper of relect.DeepEqual() which would do the following operations:

  1. If the original reflect.DeepEqual() returns true , then we can directly return the boolean output.
  2. If reflect.DeepEqual() returns false, we tried testing the equality of their canonical forms. The problem was the int was getting converted to float64 after unmarshalling, so we converted both objects to their canonical forms by first marshalling [marshaling (US spelling)] them and then comparing their unmarshalling forms.
Conversion of 2 interfaces to their canonical forms
import (
   "encoding/json"
   "reflect"
)
func DeepEqual(v1, v2 interface{}) bool {
   if reflect.DeepEqual(v1, v2) {
      return true
   }
   var x1 interface{}
   bytesA, _ := json.Marshal(v1)
   _ = json.Unmarshal(bytesA, &x1)
   var x2 interface{}
   bytesB, _ := json.Marshal(v2)
   _ = json.Unmarshal(bytesB, &x2)
   if reflect.DeepEqual(x1, x2) {
      return true
   }
return false
}

Let’s use this wrapper to see if this has fixed our problem or not.

package main
import (
   "encoding/json"
   "fmt"
   "os"
)
type School struct {
   Id   interface{}
   Name string
}
func main() {
   x := School{
      Id:   1,
      Name: "Golang Public School",
   }
   bytes, err := json.Marshal(x)
   if err != nil {
      println("error while unmarshalling the json ", err)
      os.Exit(1)
   }
   var y School
   err = json.Unmarshal(bytes, &y)
   if err != nil {
      println("error while unmarshalling the json ", err)
      os.Exit(1)
   }
   if DeepEqual(x, y) {
      println("both x & y are same")
   } else {
      println("x & y are different")
   }
   fmt.Println("value of x is ", x)
   fmt.Println("value of y is ", y)
}

Let’s see what’s the output now:

$ go run main.go
both x & y are same
value of x is  {1 Golang Public School}
value of y is  {1 Golang Public School}

You may come across many problems in computer science where two things need to be compared, converting them to their canonical forms proves to be super useful.

Thanks to Nipun Jindal for working with me on this.

Click here to read more stories about how we do what we do.

And we’re hiring! Check out the link below: