Go Comparable Type原理深入解析
作者:sorcererxw 发布时间:2024-02-21 03:47:16
介绍
在 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 内置类型的约定:
Type | Comparable | Ordered | Description |
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, 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)
}
现实中的陷阱与应用
在知道上面的设定之后,可以理解很多我们在开发当中碰到的错误。
errors.Is
我们常常在模块内定义错误时,会定义出如下类型:
type CustomError struct {
Metadata map[string]string
Message string
}
func (c CustomError) Error() string {
return c.Message
}
var (
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。研究一下 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) {
return 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.
如上描述,如果不加上这一段约束,会引发 panic。
所以如果我们把一个 map 放入了 error struct,就导致这个 error 变为 incomparable,永远无法成功比较。
解决方案也很简单,就是将 Error 定义指针类型:
var (
ErrorA = &CustomError{Message:"A", Matadata: map[string]string{"Reason":""}}
ErrorB = &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 error.
}
上面返回的 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,可以减少内存开销。
来源:https://sorcererxw.com/articles/go-comparable-type
猜你喜欢
- 作为一个新世纪有思想有文化有道德时刻准备着的 * 丝男青年,在现在这样一个社会中,心疼我大慢播抵制大百度的前提下,没事儿上上网逛逛YY看看斗鱼翻
- django settings.py 配置文件import osBASE_DIR = os.path.dirname(os.path.dir
- python中可以使用下标索引来访问列表中的值,对列表进行切片即截取,也可以对列表的数据项进行修改或更新。使用下标索引来访问列表中的值,例如
- 一. 什么是Selenium?网络爬虫是Python编程中一个非常有用的技巧,它可以让您自动获取网页上的数据。在本文中,我们将介绍如何使用S
- 今天有点囧a=['XXXX_game.sql', 'XXXX_game_sp.sql', 'XXXX
- 前言对于数据库中的树形结构数据,如部门表,有时候,我们需要知道某部门的所有下属部分或者某部分的所有上级部门,这时候就需要用到mysql的递归
- 组合集总计: group by with rollup/cube grouping sets 子查询按执行方式分:标准子查询、关联子查询 标
- subplot(arg1, arg2, arg3)arg1: 在垂直方向同时画几张图arg2: 在水平方向同时画几张图arg3: 当前命令修
- 前言深度学习涉及很多向量或多矩阵运算,如矩阵相乘、矩阵相加、矩阵-向量乘法等。深层模型的算法,如BP,Auto-Encoder,CNN等,都
- 请定义函数,将列表[10, 1, 2, 20, 10, 3, 2, 1, 15, 20, 44, 56, 3, 2, 1]中的重复元素除去,
- Python 类的继承详解Python既然是面向对象的,当然支持类的继承,Python实现类的继承比JavaScript简单。Parent类
- Any docsAny 是一种特殊的类型。静态类型检查器将所有类型视为与 Any 兼容,反之亦然, Any 也与所有类型相兼容。这意味着可对
- 网上找了半天 不是dataframe转化成array的就是array转化dataframe,所以这里给汇总一下,相互转换的python代如下
- Golang 性能基准测试Golang 中的性能基准测试是使用标准库 testing 来实现的,编写性能测试代码是很容易的:创建性能测试文件
- 本文实例讲述了Python实现的微信公众号群发图片与文本消息功能。分享给大家供大家参考,具体如下:在微信公众号开发中,使用api都要附加ac
- 简介增量备份是指在一次全备份或上一次增量备份后,以后每次的备份只需备份与前一次相比增加或者被修改的文件。这就意味着,第一次增量备份的对象是进
- 如下所示:df = df[df['cityname']==u'北京市']记得,如果用的python2,一定要
- 本文实例讲述了Python中列表元素转为数字的方法。分享给大家供大家参考,具体如下:有一个数字字符的列表:numbers = ['1
- map()是python的一个内建函数, 他能够通过函数来处理序列,比如,我们相关一个数组[0,1,2,3,4,5]所有的数字都+2 , 当
- Conv2d的简单使用torch 包 nn 中 Conv2d 的用法与 tensorflow 中类似,但不完全一样。在 torch 中,Co