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

相关推荐
程序员爱钓鱼2 小时前
Go语言实战案例-创建模型并自动迁移
后端·google·go
javachen__2 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
阿珊和她的猫4 小时前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
uzong7 小时前
技术故障复盘模版
后端
GetcharZp8 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
加班是不可能的,除非双倍日工资8 小时前
css预编译器实现星空背景图
前端·css·vue3
桦说编程8 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研8 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi8 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip9 小时前
vite和webpack打包结构控制
前端·javascript