TL;DR, I propose adding github.com/google/go-cmp/cmp to the standard library as testing/cmp.
Determining equality of two values is one of the most common operations needed for a unit test where the test wants to know whether some computed value matches some pre-determined expected value.
Using the Go language and standard library alone, there are two primary options:
For simple cases, these work fine, but are insufficient for more complex cases:
- The
== operator only works on comparable types, which means that it doesn't work for Go slices and maps, which are two common kinds that tests want to compare.
- The
reflect.DeepEqual function is a "recursive relaxation of Go's == operator", but has several short-comings:
- It provides no ability to customize the comparison, which is increasingly more useful for more complex types.
- It does not explain why two values are different, which becomes increasingly more helpful for larger values.
- It blindly compares unexported fields, which causes a test to unfortunately have an implicit dependency on unexported artifacts of types from a module's dependencies (and therefore on undefined behavior).
For many users, the standard library is insufficient, so they turn to third-party packages to perform equality. According to the module proxy, the most common comparison module used is github.com/google/go-cmp with 7264 module dependents (and 25th most imported module). I propose including cmp in the standard library itself.
Why include cmp in the standard library?
The most widely used comparison function in Go is currently reflect.DeepEqual (with ~34k usages of reflect.DeepEqual compared to ~6k usages of cmp.Equal). Inclusion of cmp to the standard library would provide better visibility to it and allow more tests to be written that would have been better off using cmp.Equal rather than reflect.DeepEqual.
A problem with reflect.DeepEqual is that it hampers module authors from making changes to their types that otherwise should be safe to perform. Since reflect.DeepEqual blindly compares unexported fields, it causes a test to have an implicit dependency on the value of unexported fields in types that come from a module's dependency. When the author of one those types changes an unexported field, it surprisingly breaks many brittle tests. Examples of this occurring was when Go1.9 added monotonic timestamp support and the change to the internal representation of time.Time broke hundreds of test at Google. Similar problems occurred with adding/modifying unexported fields to protocol buffer messages. reflect.DeepEqual is a comparison function that looks like it works wells, but may cause the test to be brittle. Furthermore, reflect.DeepEqual does not tell you why two values are different, making it even more challenging for users to diagnose such brittle tests.
As a contributor to the standard library, there are a number of times that I would have liked to use cmp when writing tests in the standard library itself.
How is cmp.Equal similar or different than reflect.DeepEqual?
cmp.Equal is designed to be more expressive than reflect.DeepEqual. It accepts any number of user-specified options that may affect how the comparison is performed. While reflect.DeepEqual is more performant, cmp.Equal is more flexible. Also, cmp.Diff provides a humanly readable report for why two values differ, rather than simply providing a boolean result for whether they differ.
One significant difference from reflect.DeepEqual is that cmp.Equal panics by default when trying to compare unexported fields unless the user explicitly permits it with an cmp.Exporter option. This design decision was to avoid the problems observed with using reflect.DeepEqual at scale mentioned earlier.
Without any options specified, cmp.Equal is identical to reflect.DeepEqual except:
cmp.Equal panics when trying to comparing unexported fields
cmp.Equal uses a type's Equal method if it has one
cmp.Equal has an arguably more correct comparison for graphs
reflect.DeepEqual's cycle detection primarily aims to avoid infinite recursion, but does not necessarily verify whether the two graphs have the same topology, while cmp.Equal checks that the graph topology are the same.
(Fun fact) Package reflect is the 15th most imported Go package, but ~77% of the imports (for test files) only do so to use DeepEqual. In 2011, Rob Pike explained that "[reflection] is a powerful tool that should be used with care and avoided unless strictly necessary." I find it ironic that most usages of reflect is to access a function that is arguably not even about Go reflection (i.e., it doesn't provide functionality that mirrors the Go language itself).
How does cmp compare to other comparison packages?
The only other comparison module (within the top 250 most widely used modules) is github.com/go-test/deep with 845 dependents (compared to 7264 dependents for cmp). Package deep is not as flexible as cmp and relies on globals to configure how comparisons are performed.
TL;DR, I propose adding
github.com/google/go-cmp/cmpto the standard library astesting/cmp.Determining equality of two values is one of the most common operations needed for a unit test where the test wants to know whether some computed value matches some pre-determined expected value.
Using the Go language and standard library alone, there are two primary options:
==operator from the language itself, andreflect.DeepEqualfunction.For simple cases, these work fine, but are insufficient for more complex cases:
==operator only works on comparable types, which means that it doesn't work for Go slices and maps, which are two common kinds that tests want to compare.reflect.DeepEqualfunction is a "recursive relaxation of Go's == operator", but has several short-comings:For many users, the standard library is insufficient, so they turn to third-party packages to perform equality. According to the module proxy, the most common comparison module used is
github.com/google/go-cmpwith 7264 module dependents (and 25th most imported module). I propose includingcmpin the standard library itself.Why include
cmpin the standard library?The most widely used comparison function in Go is currently
reflect.DeepEqual(with ~34k usages ofreflect.DeepEqualcompared to ~6k usages ofcmp.Equal). Inclusion ofcmpto the standard library would provide better visibility to it and allow more tests to be written that would have been better off usingcmp.Equalrather thanreflect.DeepEqual.A problem with
reflect.DeepEqualis that it hampers module authors from making changes to their types that otherwise should be safe to perform. Sincereflect.DeepEqualblindly compares unexported fields, it causes a test to have an implicit dependency on the value of unexported fields in types that come from a module's dependency. When the author of one those types changes an unexported field, it surprisingly breaks many brittle tests. Examples of this occurring was when Go1.9 added monotonic timestamp support and the change to the internal representation oftime.Timebroke hundreds of test at Google. Similar problems occurred with adding/modifying unexported fields to protocol buffer messages.reflect.DeepEqualis a comparison function that looks like it works wells, but may cause the test to be brittle. Furthermore,reflect.DeepEqualdoes not tell you why two values are different, making it even more challenging for users to diagnose such brittle tests.As a contributor to the standard library, there are a number of times that I would have liked to use
cmpwhen writing tests in the standard library itself.How is
cmp.Equalsimilar or different thanreflect.DeepEqual?cmp.Equalis designed to be more expressive thanreflect.DeepEqual. It accepts any number of user-specified options that may affect how the comparison is performed. Whilereflect.DeepEqualis more performant,cmp.Equalis more flexible. Also,cmp.Diffprovides a humanly readable report for why two values differ, rather than simply providing a boolean result for whether they differ.One significant difference from
reflect.DeepEqualis thatcmp.Equalpanics by default when trying to compare unexported fields unless the user explicitly permits it with ancmp.Exporteroption. This design decision was to avoid the problems observed with usingreflect.DeepEqualat scale mentioned earlier.Without any options specified,
cmp.Equalis identical toreflect.DeepEqualexcept:cmp.Equalpanics when trying to comparing unexported fieldscmp.Equaluses a type'sEqualmethod if it has onecmp.Equalhas an arguably more correct comparison for graphsreflect.DeepEqual's cycle detection primarily aims to avoid infinite recursion, but does not necessarily verify whether the two graphs have the same topology, whilecmp.Equalchecks that the graph topology are the same.(Fun fact) Package
reflectis the 15th most imported Go package, but ~77% of the imports (for test files) only do so to useDeepEqual. In 2011, Rob Pike explained that "[reflection] is a powerful tool that should be used with care and avoided unless strictly necessary." I find it ironic that most usages ofreflectis to access a function that is arguably not even about Go reflection (i.e., it doesn't provide functionality that mirrors the Go language itself).How does
cmpcompare to other comparison packages?The only other comparison module (within the top 250 most widely used modules) is
github.com/go-test/deepwith 845 dependents (compared to 7264 dependents forcmp). Packagedeepis not as flexible ascmpand relies on globals to configure how comparisons are performed.