前言
软件开发中对两个值进行比较是非常基础的操作,通常要实现一个比较函数,该函数接收两个待比较的对象,函数内部比较这两个对象是否相等。比较操作第一想法是使用 == 操作符。但通过下面的例子可以看到,有些情况下不能使用 == 操作判断两个对象是否相等。具体什么时候可以使用 == 什么时候不能使用,且看下文分析。
案例引入
下面定义了一个 customer 结构体,通过 == 比较操作判断两个customer对象是否相等,猜猜程序输出结果是true还是false.
golang
type customer struct {
id string
}
func main() {
cust1 := customer{id: "x"}
cust2 := customer{id: "x"}
fmt.Println(cust1 == cust2)
}
通过比较操作符 == 对两个customer对象进行比较是正确的操作,程序输出结果为 true. 现在对customer结构体稍微做一点修改,向里面添加一个切片字段。修改后的代码如下。
golang
type customer struct {
id string
operations []float64
}
func main() {
cust1 := customer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(cust1 == cust2)
}
上述程序是输出true还是false呢?都不是,编译会报如下错误。原因与比较操作符 == 和 != 有关,它们不能在切片和map上使用,即不能将两个切片或map对象通过==判断是否相等。customer结构体中包含切片字段operations, 所以它不能编译。
console
invalid operation:
cust1 == cust2 (struct containing []float64 cannot be compared)
原因分析
理解 == 和 != 操作符能作用的对象类型是进行有效比较的关键,下面列举了可以使用比较操作的类型。
-
布尔类型(bool),比较两个布尔类型的变量是否相等
-
数值类型(int,float和复数),比较两个数值是否相等
-
字符串类型(string), 比较两个字符串是否相等
-
通道类型(channel),如果通道都是nil,则它们相等,两次make创建的通道则不相等,实例程序如下。输出结果为 true, false, true
golang
var ch1, ch2 chan int
ch3 := make(chan int, 2)
ch4 := make(chan int, 2)
ch5 := ch3
fmt.Println(ch1 == ch2)
fmt.Println(ch3 == ch4)
fmt.Println(ch5 == ch3)
- 接口类型(interface), 如果两个接口都为nil,则它们相等,如果接口的类型和值都相等,则它们相等。下面代码输出结果都是false.
golang
var ifc1 interface{} = int(0)
var ifc2 interface{} = int32(0)
fmt.Println(ifc1 == ifc2)
var ifc3 interface{} = int(0)
var ifc4 interface{} = int(1)
fmt.Println(ifc3 == ifc4)
-
指针类型(pointer),如果两个指针指向内存中相同的位置,或者都为nil,则它们相等。
-
结构体和数组,是否相等由构成它们的字段类型决定。
NOTE 对于数值类型和字符串类型,还可以使用 >=、< 和 >操作符, 字符串大小是根据字母顺序定义的。
any类型比较
此外,我们需要了解在 any 类型对象上进行 == 和 != 操作存在的问题。下面代码将整数赋值给 any类型变量,然后比较是否相等。程序输出为true.
golang
var a any = 3
var b any = 3
fmt.Println(a == b)
但是如果赋值给any类型变量的是上面提到的 customer对象,虽然不会产生编译时错误,但是运行时有问题。
golang
var cust1 any = customer{id: "x", operations: []float64{1.}}
var cust2 any = customer{id: "x", operations: []float64{1.}}
fmt.Println(cust1 == cust2)
报错信息如下,因为customer结构体中含有不可比较的切片字段。
console
panic: runtime error: comparing uncomparable type main.customer
复杂结构如何进行比较
那怎么才能对两个切片或map对象进行比较呢,或者说结构体中包含非比较类型的字段,如何处理呢? 一种方法是使用标准库reflect包中提供的运行时反射。反射是元编程的一种形式,应用程序有能力在程序运行时修改其结构和行为。在Go语言中,我们可以使用 reflect.DeepEqual 方法比较两个对象是否相等。它通过递归遍历两个值是否相等,即使结构体中含有数组、切片、map、指针、接口和函数字段类型也是可比较的。
NOTE reflect.DeepEqual有些特性跟对象的类型有关,在使用之前,需要认真阅读官方文档说明。
现在使用 reflect.DeepEqual 对上面的customer对象进行比较,代码如下,运行输出结果如预期为true.
golang
cust1 := customer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(reflect.DeepEqual(cust1, cust2))
注意,在使用reflect.DeepEqual时,要小心两件事情。一是集合类型(像slice),空slice和nil是不相同的。下面程序输出结果为false. 关于空slice和nil在 Go语言中常见100问题-#22 空切片与nil切片最佳实践 有详细说明。
golang
var s1 []string
s2 := make([]string, 0)
fmt.Println(reflect.DeepEqual(s1, s2))
在某些场合下,这不是一个问题。例如,我们想比较两个unmarshaling(JSON字符串转Go结构体)后的对象是否相同,空slice和nil不同正是我们预期需要的结果,记得在这种情况使用reflect.DeepEqual。
二是要知道reflect.DeepEqual内部使用了反射,会有性能损失,其他语言也有类似的性能问题。通过基准测试得到结果是DeepEqual比==大约慢100倍。所以一般在测试代码使用reflect.DeepEqual,而在对性能要求比较高的正式代码中不建议使用。
自定义比较函数
如果对性能有比较高的要求,我们可以实现自定义的比较函数。下面是比较两个customer对象是否相同的示例函数。
golang
func (a customer) equal(b customer) bool {
if a.id != b.id {
return false
}
if len(a.operations) != len(b.operations) {
return false
}
for i := 0; i < len(a.operations); i++ {
if a.operations[i] != b.operations[i] {
return false
}
}
return true
}
创建两个含有100个operation元素的customer对象,在本地执行基准测试测得自定义的equal比较函数比reflect.DeepEqual块96倍。
思考总结
我们应该知道 == 比较操作符是有局限性的,并不是任何对象都可以进行比较,比如它就不能对slice和map进行比较。在大多数情况下,采用reflect.DeepEqual比较是一个不错的方法,缺点是有性能损失。在单元测试代码中,我们也可以使用go-cmp(github.com/google/go-c...
此外,要记得标准库中还存在其他比较函数。例如,可以使用bytes.Compare比较两个bytes类型的切片。在实现自定义比较函数之前,先看看标准库中是否已有提供,避免重复造轮子。