前段时间,有读者发给我一个Go代码示例,问我是不是bug。觉得挺有意思的,所以整理了这篇文章的分享内容。在讨论代码之前,读者需要一些先验知识。Go的可比较类型在Go中,数据类型可以分为两类,可比较的和不可比较的。两者之间的区别很简单:是否可以使用运算符==和!=来比较类型。哪些类型具有可比性?Boolean(布尔值)、Integer(整数)、Floating-point(浮点数)、Complex(复数)和String(字符)无疑是可比的。Poniters(指针)可以进行比较:如果两个指针指向同一个变量,或者两个指针属于同一类型且值为nil,则它们相等。请注意,指向不同的零大小变量的指针可能相等也可能不相等。通道是可比较的:如果两个通道值是由同一个make调用创建的,则它们是相等的。c1:=make(chanint,2)c2:=make(chanint,2)c3:=c1fmt.Println(c3==c1)//truefmt.Println(c2==c1)//false接口(接口值)有可比性:如果两个接口值具有相同的动态类型和相等的动态值,则它们是相等的。Avaluexofanon-interfacetypeXandavaluetofaninterfacetypeTarecomparablewhenvaluesoftypeXarecomparableandXimplementsT.如果t的动态类型与X相同且t的动态类型相同,则它们相等值等于x。struct(structvalues)arecomparableifallfieldsarecomparable:两个结构值相等,如果它们对应的非空字段相等。如果数组的元素类型的值是可比较的,则数组值是可比较的:如果两个数组值对应的元素相等,则它们是相等的。哪些类型不可比较?slice、map、function是不可比较的,但是有特殊情况,就是当它们的值为nil时,可以和nil进行比较。动态类型在上面的接口对比中,我们提到了动态类型和动态值,这里需要介绍一下。常规变量(不是接口)的类型由声明定义,它是静态类型,例如变量x整数。接口类型的变量具有唯一的动态类型,即运行时存储在变量中的值的实际类型。动态类型在执行过程中可能会发生变化,但始终可以将静态类型分配给接口变量。比如varsomeVariableinterface{}=101变量some??Variable的静态类型是interface{},但是它的动态类型是int,后面很可能会变。varsomeVariableinterface{}=101someVariable='Gopher'如上,someVariable变量的动态类型由int变为string。代码场景示例我们为当前业务需要的数据模型定义一个结构体Data,它包含两个字段:string类型的UUID和interface{}类型的Content。typeDatastruct{UUIDstringContentinterface{}}根据上面的介绍,string类型和interface是可比较的类型,那么两个Data类型的数据,我们可以使用==运算符进行比较。varx,yDatax=Data{UUID:"856f5555806443e98b7ed04c5a9d6a9a",Content:1,}y=Data{UUID:"745dee7719304991862e6985ea9c02a9",Content:2,}fmt.Println(x==ytype)但是,如果在动态ContentWhat关于地图?varm,nDatam=Data{UUID:"9584dba3fe26418d86252d71a5d78049",Content:map[int]string{1:"GO",2:"Python"},}n=Data{UUID:"9584dba3fe26418d86252d71a5d78049",Content:map[int]string{1:"GO",2:"Python"},}fmt.Println(m==n)至此,我们的程序编译成功,但是会出现运行时错误。panic:runtimeerror:comparinguncomparabletypemap[int]string针对的是这个需求:即对于不可比较的类型,因为不能使用比较运算符==,但是我们要比较它们包含的值是否相等,我们应该怎么办做?此时我们可以使用reflect.DeepEqual函数进行比较,即将上面的m==n换成fmt.Println(reflect.DeepEqual(m,n))//true可以得出结论,如果我们的变量包含不可比较的类型,或者接口类型(其动态类型可能不可比较),那么我们直接使用比较运算符==,这将导致程序错误。这时候就应该用到reflect.DeepEqual函数(当然也有特殊情况,比如[]byte,可以通过bytes.Equal函数进行比较)。错误代码?好了,铺垫了这么久,终于可以展示一下读者给我的代码了。varx,yDatax=Data{UUID:"856f5555806443e98b7ed04c5a9d6a9a",Content:1,}bytes,_:=json.Marshal(x)_=json.Unmarshal(bytes,&y)fmt.Println(x)//{856f5555806443e95at19d04c.Printl(y)//{856f5555806443e98b7ed04c5a9d6a9a1}fmt.Println(reflect.DeepEqual(x,y))//false同样的原始数据,经过json的Marshal和Unmarshal处理后,不相等?有错误吗?不要着急,这个时候我们直接去调试。调试发现这个1不是另外1个,Content字段的数据类型由int转为float64。在一个接口中,当它的动态类型不一致时,它的比较是不相等的。经过排查,发现问题出在Unmarshal函数上:如果要将Json对象作为接口值进行unmarshal,那么其类型转换规则如下Unmarshal可以看到数值json解析操作统一为float64。因此,如果我们将Content:1更改为Content:1.0那么它会reflect.DeepEqual(x,y)为真。增强的DeepEqual函数就是针对json解析的这种类型变化特性,我们可以在reflect.DeepEqual函数的基础上进行适配。funcDeepEqual(v1,v2interface{})bool{ifreflect.DeepEqual(v1,v2){returntrue}bytesA,_:=json.Marshal(v1)bytesB,_:=json.Marshal(v2)returnbytes.Equal(bytesA,bytesB)}当我们使用增强功能运行上面的“bug”示例时b,&y)fmt.Println(DeepEqual(x,y))//true至此,结果符合我们的预期。结论本文讨论了Go的可比较和不可比较类型,并解释了静态和动态类型。不可比较的类型包括slice、map、function,不能用==来比较。虽然我们可以通过==运算符来比较接口,但是由于动态类型的存在,如果实现接口的T有一个无法比较的类型,就会导致运行时错误。在无法确定接口的实现类型的情况下,可以使用reflect.DeepEqual函数进行接口比较。最后,通过json库的解析和反解析过程,我们发现json解析存在数据类型转换的操作。读者在使用过程中需要注意这个细节,以免认为“这段代码有bug”。参考https://golang.org/ref/spec#Comparison_operatorshttps://golang.org/ref/spec#Typeshttps://pkg.go.dev/encoding/json#Unmarshal
