Go语言中常见100问题-#29 比较操作误区与解决方法

前言

软件开发中对两个值进行比较是非常基础的操作,通常要实现一个比较函数,该函数接收两个待比较的对象,函数内部比较这两个对象是否相等。比较操作第一想法是使用 == 操作符。但通过下面的例子可以看到,有些情况下不能使用 == 操作判断两个对象是否相等。具体什么时候可以使用 == 什么时候不能使用,且看下文分析。

案例引入

下面定义了一个 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类型的切片。在实现自定义比较函数之前,先看看标准库中是否已有提供,避免重复造轮子。

相关推荐
m0_7482356111 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
超爱吃士力架9 小时前
邀请逻辑
java·linux·后端
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式