Equality in Golang

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

Michał Łowicki
Feb 26, 2020 · 8 min read

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):

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

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):

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.

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):

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):

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):

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):

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

Output:

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

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

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.

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

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

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):

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):

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

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):

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.

This function is generic method to compare any values:

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

and slices (source code):

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

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

Output:

Please check documentation out to learn more.

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

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.

golangspec

A series dedicated to deeply understand Go’s specification…