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 Id
as 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.
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:
- If the original
reflect.DeepEqual()
returnstrue
, then we can directly return the boolean output. - If
reflect.DeepEqual()
returns false, we tried testing the equality of their canonical forms. The problem was theint
was getting converted tofloat64
after unmarshalling, so we converted both objects to their canonical forms by first marshalling [marshaling (US spelling)] them and then comparing their unmarshalling 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: