Equality in Golang

Tale of comparison operators and {DeepEqual,Equal,EqualFold} methods

Michał Łowicki
golangspec
Published in
8 min readFeb 26, 2020

--

You’ve probably seen == or != operators many times in .go files. It turns out that some types like e.g. maps or slices can’t be used as operands to these operators. It’s sometimes needed to compare such values though. In this post we’ll explore all rules behind equality operators in Go, how it plays with Unicode and what are the methods to compare non-comparable types.

Let’s start with something simple and actually not hiding many secrets. For booleans and numeric types (floats, integers, complex numbers) equality operators work without bigger surprises. Type float64 has few special values like NaN (IEEE 754 “not a number” value), positive and negative infinity. It’s worth to mention that NaN is not equal to NaN (source code):

nan := math.NaN()
pos_inf := math.Inf(1)
neg_inf := math.Inf(-1)
fmt.Println(nan == nan) // false
fmt.Println(pos_inf == pos_inf) // true
fmt.Println(neg_inf == neg_inf) // true
fmt.Println(pos_inf == neg_inf) // false

Pointers

Two pointers are equal if either both are nil or both point to exactly the same address in memory (source code):

var p1, p2 *string
name := "foo"
fmt.Println(p1 == p2) // true
p1 = &name
p2 = &name
fmt.Println(p1) // 0x40c148
fmt.Println(p2) // 0x40c148
fmt.Println(&p1) // 0x40c138
fmt.Println(&p2) // 0x40c140
fmt.Println(*p1) // foo
fmt.Println(*p2) // foo
fmt.Println(p1 == p2) // true

Language spec also says:

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

Let’s see that rule in action (source code):

type S struct{}func main() {
var p1, p2 *S
s1 := S{}
s2 := S{}
p1 = &s1
p2 = &s2
fmt.Printf("%p\n", p1) // 0x1e52bc
fmt.Printf("%p\n", p2) // 0x1e52bc
fmt.Println(p1) // &{}
fmt.Println(p2) // &{}
fmt.Println(&p1) // 0x40c138
fmt.Println(&p2) // 0x40c140
fmt.Println(*p1) // {}
fmt.Println(*p2) // {}
fmt.Println(p1 == p2) // true
}

By changing S type definition to non-empty struct like S struct {f int} you’ll see that p1 and p2 are not equal anymore — source code.

Channels

With basic concurrency primitive in Go we’ve two rules i.e. two channels are equal when either:

  • both are nil
  • both are created by the same call to built-in function make

Code snippet above demonstrates this behaviour (source code):

func f(ch1 chan int, ch2 *chan int) {
fmt.Println(ch1 == *ch2) // true
}
func main() {
var ch1, ch2 chan int
fmt.Println(ch1 == ch2) // true
ch1 = make(chan int)
ch2 = make(chan int)
fmt.Println(ch1 == ch2) // false
ch2 = ch1
fmt.Printf("%p\n", &ch1) // 0x40c138
fmt.Printf("%p\n", &ch2) // 0x40c140
fmt.Println(ch1 == ch2) // true
f(ch1, &ch1)
}

Interfaces

First case might seem simple — two interface values are equal if both are nil. It’s important to remember when exactly interface value is nil. It happens when both dynamic type and dynamic value are nil (source code):

type I interface{ m() }type T []bytefunc (t T) m() {}func main() {
var t T
fmt.Println(t == nil) // true
var i I = t
fmt.Println(i == nil) // false
fmt.Println(reflect.TypeOf(i)) // main.T
fmt.Println(reflect.ValueOf(i).IsNil()) // true
}

More about interfaces in earlier series — https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c.

If dynamic types are identical and dynamic values are equal then two interface values are equal (source code):

type A int
type B = A
type C int
type I interface{ m() }func (a A) m() {}func (c C) m() {}func main() {
var a I = A(1)
var b I = B(1)
var c I = C(1)
fmt.Println(a == b) // true
fmt.Println(b == c) // false
fmt.Println(a == c) // false
}

It’s possible to compare value x of non-interface type X with value i of interface type I . There are few limitations though:

  • type X implements interface I
  • type X is comparable

If dynamic type of i is X and dynamic value of i is equal to x then values are equal (source code):

type I interface{ m() }type X intfunc (x X) m() {}type Y intfunc (y Y) m() {}type Z intfunc main() {
var i I = X(1)
fmt.Println(i == X(1)) // true
fmt.Println(i == Y(1)) // false
// fmt.Println(i == Z(1)) // mismatched types I and C
// fmt.Println(i == 1) // mismatched types I and int
}

If dynamic types of interface values are identical but not comparable then it will generate runtime panic (source code):

type A []bytefunc main() {
var i interface{} = A{}
var j interface{} = A{}
fmt.Println(i == j)
}

Output:

panic: runtime error: comparing uncomparable type main.A

If types are different but still not comparable then interface values aren’t equal (source code):

type A []byte
type B []byte
func main() {
// A{} == A{} // slice can only be compared to nil
var i interface{} = A{}
var j interface{} = B{}
fmt.Println(i == j) // false
}

Structs

While comparing structs, corresponding non-blank fields are checked for equality — both exported and non-exported (source code):

type A struct {
_ float64
f1 int
F2 string
}
type B struct {
_ float64
f1 int
F2 string
}
func main() {
fmt.Println(A{1.1, 2, "x"} == A{0.1, 2, "x"}) // true
// fmt.Println(A{} == B{}) // mismatched types A and B
}

It’s worth to introduce now main rule applicable not only for structs but all types:

x == y is allowed only when either x is assignable to y or y is assignable to x.

This is why A{} == B{} above generates compile-time error.

Arrays

This is similar to struct explained earlier. Corresponding elements needs to be equal for the whole arrays to be equal (source code):

type T struct {
name string
age int
_ float64
}
func main() {
x := [...]float64{1.1, 2, 3.14}
fmt.Println(x == [...]float64{1.1, 2, 3.14}) // true
y := [1]T{{"foo", 1, 0}}
fmt.Println(y == [1]T{{"foo", 1, 1}}) // true
}

Strings

Strings are in effect immutable slices of bytes and equality works for them by running comparison byte by byte (source code):

fmt.Println(strings.ToUpper("ł") == "Ł")     // true
fmt.Println("foo" == "foo") // true
fmt.Println("foo" == "FOO") // false
fmt.Println("Michał" == "Michal") // false
fmt.Println("żondło" == "żondło") // true
fmt.Println("żondło" != "żondło") // false
fmt.Println(strings.EqualFold("ąĆź", "ĄćŹ")) // true

Comparing something non-comparable

https://github.com/egonelbre

In this bucket we’ve three types: functions, maps and slices. We can’t do much about the functions. There is not way to compare them in Go (source code):

f := func(int) int { return 1 }
g := func(int) int { return 2 }
f == g

It generates compile-time error: invalid operation: f == g (func can only be compared to nil). It also gives a hint that we can compare functions to nil. The same is true for maps and slices (source code):

f1 := func(int) int { return 1 }
m1 := make(map[int]int)
s1 := make([]byte, 10)
fmt.Println(f1 == nil) // false
fmt.Println(m1 == nil) // false
fmt.Println(s1 == nil) // false
var f2 func()
var m2 map[int]int
var s2 []byte
fmt.Println(f2 == nil) // true
fmt.Println(m2 == nil) // true
fmt.Println(s2 == nil) // true

Are there any options for maps or slices though? Luckily there are and we’ll explore them right now…

[]byte

Package bytes offers utilities to deal with byte slices and it provides functions to check if slices are equal and even equal under Unicode case-folding (source code):

s1 := []byte{'f', 'o', 'o'}
s2 := []byte{'f', 'o', 'o'}
fmt.Println(bytes.Equal(s1, s2)) // true
s2 = []byte{'b', 'a', 'r'}
fmt.Println(bytes.Equal(s1, s2)) // false
s2 = []byte{'f', 'O', 'O'}
fmt.Println(bytes.EqualFold(s1, s2)) // true
s1 = []byte("źdźbło")
s2 = []byte("źdŹbŁO")
fmt.Println(bytes.EqualFold(s1, s2)) // true
s1 = []byte{}
s2 = nil
fmt.Println(bytes.Equal(s1, s2)) // true

What about maps or slices where elements of underlying arrays are not bytes? We’ve two options: reflect.DeepEqual , cmp package or writing ad-hoc comparison code using e.g. for statement. Let’s see first two approaches in action.

reflect.DeepEqual

This function is generic method to compare any values:

func DeepEqual(x, y interface{}) bool

Let’s see how it works with maps (source code):

m1 := map[string]int{"foo": 1, "bar": 2}
m2 := map[string]int{"foo": 1, "bar": 2}
// fmt.Println(m1 == m2) // map can only be compared to nil
fmt.Println(reflect.DeepEqual(m1, m2)) // true
m2 = map[string]int{"foo": 1, "bar": 3}
fmt.Println(reflect.DeepEqual(m1, m2)) // false
m3 := map[string]interface{}{"foo": [2]int{1,2}}
m4 := map[string]interface{}{"foo": [2]int{1,2}}
fmt.Println(reflect.DeepEqual(m3, m4)) // true
var m5 map[float64]string
fmt.Println(reflect.DeepEqual(m5, nil)) // false
fmt.Println(m5 == nil) // true

and slices (source code):

s := []string{"foo"}
fmt.Println(reflect.DeepEqual(s, []string{"foo"})) // true
fmt.Println(reflect.DeepEqual(s, []string{"bar"})) // false
s = nil
fmt.Println(reflect.DeepEqual(s, []string{})) // false
s = []string{}
fmt.Println(reflect.DeepEqual(s, []string{})) // true

You can even apply it to types covered earlier like structs (source code):

type T struct {
name string
Age int
}
func main() {
t := T{"foo", 10}
fmt.Println(reflect.DeepEqual(t, T{"bar", 20})) // false
fmt.Println(reflect.DeepEqual(t, T{"bar", 10})) // false
fmt.Println(reflect.DeepEqual(t, T{"foo", 10})) // true
}

cmp package

Package cmp offers additional capabilities like customisable Equal methods , option to ignore non-exported struct fields or get diff of two values (source code):

import (
"fmt"
"github.com/google/go-cmp/cmp"
)
type T struct {
Name string
Age int
City string
}
func main() {
x := T{"Michał", 99, "London"}
y := T{"Adam", 88, "London"}
if diff := cmp.Diff(x, y); diff != "" {
fmt.Println(diff)
}
}

Output:

  main.T{
- Name: "Michał",
+ Name: "Adam",
- Age: 99,
+ Age: 88,
City: "London",
}

Please check documentation out to learn more.

Timing attacks

To prevent timing attacks there’s a standard package with function where time to compare slices is independent of the parameters’ content.

import (
"bytes"
"crypto/subtle"
"testing"
)
var (
x = []byte{'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}
y = []byte{'b', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}
z = []byte{'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'b'}
)
func BenchmarkBytesEqualXY(b *testing.B) {
for n := 0; n < b.N; n++ {
bytes.Equal(x, y)
}
}
func BenchmarkBytesEqualXZ(b *testing.B) {
for n := 0; n < b.N; n++ {
bytes.Equal(x, z)
}
}
func BenchmarkConstTimeCompXY(b *testing.B) {
for n := 0; n < b.N; n++ {
subtle.ConstantTimeCompare(x, y)
}
}
func BenchmarkConstTimeCompXZ(b *testing.B) {
for n := 0; n < b.N; n++ {
subtle.ConstantTimeCompare(x, z)
}
}
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/mlowicki/subtle
BenchmarkBytesEqualXY-12 554047392 2.09 ns/op
BenchmarkBytesEqualXZ-12 274583632 4.38 ns/op
BenchmarkConstTimeCompXY-12 66717505 15.0 ns/op
BenchmarkConstTimeCompXZ-12 70632979 15.6 ns/op

Time to compare x and y using bytes.Equal is doubled compared to x and z so it clearly depends on the content of parameters as length is always the same in tests above. That difference is negligible when using subtle.ConstantTimeCompare.

👏👏👏 below to help others discover this story. Please follow me here or on Twitter if you want to get updates about new posts or boost work on future stories.

Resources

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.