introduce
There is a Comparable definition for Type in the Go reflect package:
package reflect type Type interface { // Comparable reports whether values of this type are comparable. Comparable() bool }
As literally, Comparable means whether a type can be compared directly using operators.Go specAll comparable types are listed, and the comparability is divided into two dimensions (if the requirements do not meet the requirements, an error will be reported directly during the compilation period):
- Comparable: You can use == and != to compare, either black or white
- Ordered: You can use > >= < <= for size comparison, with a clear concept of size
I've briefly sorted out all the conventions for built-in Go types:
Type | Comparable | Ordered | Description |
Boolean | ✅ | ❌ | |
Integer | ✅ | ✅ | |
Float | ✅ | ✅ | |
Complex | ✅ | ❌ | Comparing real numbers and imaginary numbers respectively, when equal at the same time, the two complex numbers are equal. If you need to compare sizes, developers need to compare real and imaginary numbers respectively. |
String | ✅ | ✅ | Compare byte based on bytes. |
Pointer | ✅ | ❌ | If two pointers point to the same object or both are nil, the two are equal. |
Channel | ✅ | ❌ | Similar to Pointer, two Channel variables are only equal when both are nil or point to the same Channel. |
Interface | ✅ | ❌ | When the Type and Value values of two interfaces are equal at the same time, the two are equal. |
Struct | ⚠️ | ❌ | This Struct is Comparable only if all members of the Struct are Comparable. If the two struct types are the same and all non-null member variables are equal, the two are equal. |
Array | ⚠️ | ❌ | Array is Comparable only if the member is Comparable. If each element of the two Arrays is equal one by one, the two Arrays are equal. |
Map | ❌ | ❌ | |
Slice | ❌ | ❌ | |
Func | ❌ | ❌ |
As can be seen from the above, most types in Go can be compared with each other using operators, but they do not include Slice, Map and Func. There are also container types Struct and Array itself. Comparable depends on the member type.
Internal implementation
After knowing the syntax convention, we can take a look at how reflect determines the Comparable property of a variable:
type rtype struct { // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(, ) bool } func (t *rtype) Comparable() bool { return != nil }
It's very simple. In fact, it is to equip each type with an equal comparison function. If there is this function, it is comparable.
The rtype structure above is included in all types of memory headers:
// emptyInterface is the header for an interface{} value. type emptyInterface struct { typ *rtype word }
So if you want to know a certain type of equal, you need to read the corresponding type source code. Comparison functions of the corresponding type can be found by compiling SSA.
For example, ingo/src/runtime/You can see the specific implementation of the equal function of interface:
func efaceeq(t *_type, x, y ) bool { if t == nil { return true } eq := if eq == nil { panic(errorString("comparing uncomparable type " + ())) } if isDirectIface(t) { // == kindDirectIface // Direct interface types are ptr, chan, map, func, and single-element structs/arrays thereof. // Maps and funcs are not comparable, so they can't reach here. // Ptrs, chans, and single-element items can be compared directly using ==. return x == y } return eq(x, y) }
Traps and applications in reality
After knowing the above settings, we can understand many errors we encountered in development.
When we define errors in a module, we will define the following types:
type CustomError struct { Metadata map[string]string Message string } func (c CustomError) Error() string { return } var ( ErrorA = CustomError{Message:"A", Matadata: map[string]string{"Reason":""}} ErrorB = CustomError{Message:"B"} ) func DoSomething() error { return ErrorA }
After receiving an error externally, we often use to determine the error type:
err:=DoSomething() if (err, ErrorA) { // handle err }
But you will find that the above judgment is false in any case. Study the source code:
func Is(err, target error) bool { if target == nil { return err == target } isComparable := (target).Comparable() for { if isComparable && err == target { return true } if x, ok := err.(interface{ Is(error) bool }); ok && (target) { return true } if err = (err); err == nil { return false } } }
You can see that this is a recursive process on an error tree. The end condition of the truth value is err==target, but the premise is that target itself must be comparable.
A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable.
As described above, if this section of constraint is not added, panic will be triggered.
So if we put a map into an error struct, it will cause the error to become incomparable and we will never be able to successfully compare.
The solution is also very simple, which is to define the pointer type with Error:
var ( ErrorA = &CustomError{Message:"A", Matadata: map[string]string{"Reason":""}} ErrorB = &CustomError{Message:"B"} )
Comparing pointer types only requires checking whether to point to the same object, so that the comparison can be smooth.
(*Type)(nil) ≠ nil
This is from Go FAQOne of them:
func returnsError() error { var p *MyError = nil if bad() { p = ErrBad } return p // Will always return a non-nil error. }
The p returned above will never be equal to nil.
Why is this? Because error is an interface. From the above, we can see that the comparison between interfaces requires ensuring that the Type and Value of the two are equal:
- nil in the language can be understood as an interface where both Type and Value are empty
- Although the value returned in the code is empty, Type is *MyError
So p!=nil .
The correct code should look like this:
func returnsError() error { if bad() { return ErrBad } return nil }
This problem is not only a problem that occurs when an error is thrown, but any scene that returns to the interface needs to be paid attention to.
Context Value Key
Go's Context can access some global variables, and its storage method is a tree structure. Every time you take a value, it will traverse from the current node to the root node to find out whether there is a corresponding key:
func (c *valueCtx) Value(key interface{}) interface{} { if == key { return } return (key) }
Then it may occur because the key of the child node is the same as the key of one of the parent nodes, resulting in the Value being wrongly overwritten. for example:
ctx = () ctx = (ctx, "key", "123") ctx = (ctx, "key", "456") ("key") // 456
Because Context is full-link Reuters, no one can guarantee whether a key will be covered by one layer. This problem is essentially: it is too easy to "fake" a Key with the same value when the type of Key is Integer/Float/String/Complex. Then we can use the Go Comparable feature and select a type that cannot be "forged" as the key. Two more elegant ways are recommended:
Pointer type
var key = byte(0) ctx = (ctx, &key, "123") (&key)
In this way, except for in-package functions, no other code can construct the same pointer.
Struct Type
From the above, we can know that as long as strcut is of the same type and the internal values are equal, we can directly use == to judge equality. Then we can directly use struct as the Key.
type key struct {} ctx = (ctx, key{}, "123") (key{})
Similarly, we define struct as private, and we cannot construct the same key outside the package.
We know that empty structs do not occupy memory. Compared with pointer-type keys, this can reduce memory overhead.
The above is the detailed content of the in-depth analysis of the Go Comparable Type principle. For more information about the Go Comparable Type principle, please pay attention to my other related articles!