Equality in Golang
Tale of comparison operators and {DeepEqual,Equal,EqualFold} methods
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 inttype 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 []bytefunc 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
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.