开发者

Go Comparable Type原理深入解析

开发者 https://www.devze.com 2023-01-07 10:51 出处:网络 作者: sorcererxw
目录介绍内部实现现实中的陷阱与应用errors.Is(*Type)(nil) ≠ nilContext Value Key指针类型Struct 类型介绍
目录
  • 介绍
  • 内部实现
  • 现实中的陷阱与应用
    • errors.Is
    • (*Type)(nil) ≠ nil
    • Context Value Key
  • 指针类型
    • Struct 类型

      介绍

      在 Go reflect 包里面对 Type 有一个 Comparable 的定义:

      package reflect
      type Type interface {
      	// Comparable reports whether values of this type are comparable.
      	Comparable() bool
      }

      正如字面意思,Comparable 表示一个类型是否可以直接使用运算符比较。Go spec 罗列了所有可比较的类型,其中将可比较性划分为两个维度(如果不符合要求,会直接在编译期报错):

      • Comparable:可以使用 == 和 != 比较,非黑即白
      • Ordered:可以使用 > >= < <= 做大小比较,有明确的大小概念

      我简单整理了一下所有 Go 内置类型的约定:

      TypeComparableOrderedDescription
      Boolean
      Integer
      Float
      Complex分别比较实数和虚数,同时相等则两个复数相等。 如果需要比较大小,需要开发者分别比较实数和虚数。
      String基于字节逐个比较。
      Pointer如果两个指针指向同一个对象或者都为 nil,则两者相等。
      Channel类似 Pointer,两个 Channel 变量只有都为 nil,或者指向同一个 Channel 的时候才相等。
      Interface两个 interface 的 Type 和 Value 值同时相等时,两者才相等。
      Struct⚠️仅当 Struct 内所有成员都是 Comparable,这个 Struct 才是 Comparable 的。 如果两个 struct 类型相同,且所有非空成员变量都相等,则两者相等。
      Array⚠️仅当成员为 Comparable,Array 才是 Comparable 的。 如果两个 Array 中的每一个元素一一相等时,则两个 Array 相等。
      Map
      Slice
      Func

      从上面可以看到,Go 当中绝大多数类型都是可以使用运算符相互比较的,唯独不包含 Slice,Map 和 Func,也有容器类型 Struct、Array 本身的 Comparable 取决于成员的类型。

      内部实现

      知道了语法约定,我们可以看一下 reflect 具体是怎么判断一个变量的 Comparable 属性:

      type rtype struct {
      	// function for comparing objects of this type
      	// (ptr to object A, ptr to object B) -> ==?
      	equal func(unsafe.Pointer, unsafe.Pointer) bool
      }
      func (t *rtype) Comparable() bool {
      	return t.equal != nil
      }

      很简单,其实就是为每一个类型配备了一个 equal 比较函数,如果有这个函数则是 comparable。

      上面的 rtype 结构就包含在所有类型的内存头部:

      // emptyInterface is the header for an interface{} value.
      type emptyInterface struct {
      	typ  *rtype
      	word unsafe.Pointer
      }

      所以如果希望知道某一个类型的 equal 需要翻阅对应类型源码。通过编译 SSA 可以找到对应类型的比较函数。

      比如在 go/src/runtime/alg.go 下可以看到 interface 的 equal 函数的具体实现:

      func efaceeq(t *_type, x, y unsafe.Pointer) bool {
      	if t == nil {
      		return true
      	}
      	eq := t.equal
      	if eq == nil {
      		panic(errorString("comparing uncomparable type " + t.string()))
      	}
      	if isDirectIface(t) { // t.kind == kindDirectIface
      		// Direct interface types are ptr, ch编程客栈an, 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)
      }

      现实中的陷阱与应用

      在知道上面的设定之后,可以理解很多我们在开发当中碰到的错误。

      errors.Is

      我们常常在模块内定义错误时,会定义出如下类型:

      type CustomError struct {
      	Metadata map[string]string
      	Message string
      }
      func (c CustomError) Error() string {
      		return c.Message
      }
      v开发者_Python培训ar (
      	ErrorA = CustomError{Message:"A", Matadata: map[string]string{"Reason":""}}
      	ErrorB = CustomError{Message:"B"}
      )
      func DOSomething() error {
      	return ErrorA
      }

      而我们在外部接收到错误之后常常会使用 errors.Is 来判断错误类型:

      err:=DoSomething()
      if errors.Is(err, ErrorA) {
      	// handle err
      }

      但是会发现上面这个判断无论如何都是 false。研究一下&nandroidbsp;errors.Is 的源码:

      func Is(err, target error) bool {
      	if target == nil {
      		return err == target
      	}
      	isComparable := reflect.TypeOf(target).Comparable()
      	for {
      		if isComparable && err == target {
      			return true
      		}
      		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
      			retwww.devze.comurn true
      		}
      		if err = errors.Unwrap(err); err == nil {
      			return false
      		}
      	}
      }

      可以看到这是一个在 error tree 上递归的流程,真值的终结条件是 err==target ,但是前提是 target 本身得是 comparable 的。

      A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable.

      如上描述,如果不加上这一段约束,会引发 UvbdTaTNpanic。

      所以如果我们把一个 map 放入了 error struct,就导致这个 error 变为 incomparable,永远无法成功比较。

      解决方案也很简单,就是将 Error 定义指针类型:

      var (
      	ErrorA = &amp;CustomError{Message:"A", Matadata: map[string]string{"Reason":""}}
      	ErrorB = &amp;CustomError{Message:"B"}
      )

      指针类型比较只需要是否检查是否指向同一个对象,这样就能顺利比较了。

      (*Type)(nil) ≠ nil

      这是 Go FAQ 的其中一条:

      func returnsError() error {
      	var p *MyError = nil
      	if bad() {
      		p = ErrBad
      	}
      	return p // Will always return a non-nil erhttp://www.devze.comror.
      }

      上面返回的 p 永远不会与 nil 相等。

      这是为什么呢,因为 error 是一个 interface,从上面可以知道,interface 之间比较需要保证两者的 Type 和 Value 两两相等:

      • 语言内的 nil 可以理解为一个 Type 和 Value 均为空的 interface
      • 代码里面返回的 p 虽然 Value 为空,但是 Type 是 *MyError

      所以 p!=nil 。

      正确的代码应该是这样的:

      func returnsError() error {
      	if bad() {
      		return ErrBad
      	}
      	return nil
      }

      这个问题不仅仅是抛出错误的时候会出现,任何返回 interface 的场景都需要注意。

      Context Value Key

      Go 的 Context 可以存取一些全局变量,其存储方式是一个树状结构,每一次取值的时候就会从当前节点一路遍历到根节点,查找是否有对应的 Key:

      func (c *valueCtx) Value(key interface{}) interface{} {
      	if c.key == key {
      		return c.val
      	}
      	return c.Context.Value(key)
      }

      那么就可能会出现因为子节点的 Key 与其中一个父节点的 Key 相同,导致 Value 被错误地覆盖。比如:

      ctx = Context.Background()
      ctx = context.WithValue(ctx, "key", "123")
      ctx = context.WithValue(ctx, "key", "456")
      ctx.Value("key") // 456
      

      因为 Context 是全链路透传的,谁都没法保证一个 Key 是否会被其中某一层覆盖。这个问题本质上是:当Key 的类型为 Integer/Float/String/Complex 时,"伪造"一个值相同的 Key 太容易了。那么我们可以运用 Go Comparable 的特性,选择无法被"伪造"的类型作为 Key。推荐两种比较优雅的方式:

      指针类型

      var key = byte(0)
      ctx = context.WithValue(ctx, &key, "123")
      ctx.Value(&key)

      这样一来,除了包内函数,没有其他代码还能构造出相同的指针了。

      Struct 类型

      从上文可以知道,strcut 只要类型相同,内部的值相等,就能直接使用 == 判断相等,那么我们可以直接使用 struct 作为 Key。

      type key struct {}
      ctx = context.WithValue(ctx, key{}, "123")
      ctx.Value(key{})

      同样的,我们把 struct 定义为私有类似,包外也无法构造出相同的 key。

      我们知道空 struct 是不占用内存的,这么做相比指针类型的 Key,可以减少内存开销。

      以上就是Go Comparable Type原理深入解析的详细内容,更多关于Go Comparable Type原理的资料请关注我们其它相关文章!

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号