zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Go 100 mistakes之不正确的值比较

2023-02-26 09:48:24 时间

在软件开发中比较值是非常常见的操作。无论是在函数中比较两个对象,还是在单元测试中将值与期望值比较,比较操作的实现是非常频繁的。我们的第一直觉是使用 == 操作符。然而,正如我们在本节看到的,情况并非如此。那什么时候使用 == 是合适的呢?

我们从一个具体的例子开始。我们将创建一个customer结构体,并使用 == 操作符来比较两个实例。下面的代码将会输出什么呢?

type customer struct {
  id string
}

func main() {
  cust1 := customer{id: "x"}
  cust2 := customer{id: "x"}
  fmt.Println(cust1 == cust2)
}

在Go中,比较这两个customer结构体是合法的,它将会打印出true。

现在,如果我们对customer结构体稍微做下修改,在其中加入一个slice的字段,会发生什么:

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。然而,它甚至都没有编译通过:

invalid operation: cust1 == cust2 (struct containing []float64 cannot be compared)

该问题和 == 和 != 操作符的工作原理有关。了解如何使用这两个操作符以确保我们可以有效的进行比较至关重要。

如果两种类型具有可比较性,那我们可以使用这两种运算符(==和!=)来比较两种不同的类型。在Go中可比较的类型包括

  • 布尔值:== 和 != 可以比较两个布尔类型的值是否相等
  • 数字:== 和 != 可以比较两个数字类型的值是否相等。如果两个值具有相同的类型或能够转成成相同的类型,那么这两个操作也是可以正常编译的。
  • 字符串:== 和 != 可以比较两个字符串是否相等。我们可以根据字符串的词序使用>=, < 和 > 操作符对两个字符串进行比较。
  • 指针:== 和 != 可以比较两个指针是否指向了相同的内存地址或者是否都是nil。
  • 通道(channels):== 和 != 可以比较两个通道是否是由同一个make创建的或者两个都是nil

如果struct和array仅有可比较的类型组成,我们也可以将他们添加到此列表中。所以,在该列表中没有map和slice。在第一个版本中,customer结构体是由一个单一的可比较类型(一个字符串)组成的,所以使用==进行比较是合法的。相反,在第二个版本中,因为结构体customer中包含了一个slice,无法使用 == 运算符进行比较并导致了编译错误。

我们还应该知道在interface{}类型中使用 == 和 !=操作符可能存在的问题。此外,它将导致一个运行时panic:

panic: runtime error: comparing uncomparable type main.customer

那我们会有这样的疑问,如果我们不得不对slice、map、或者包含不能比较类型的struct进行比较的时候,该怎么办呢?一种方法是使用标准的库,另外一个方法就是使用反射和reflect.DeepEqual。该方法会之处两个元素是否是深度相等的。该函数接受的元素是基本类型,数组,结构体,切片(slice),map,指针,接口和函数

让我们再返回第一个例子中,这次使用reflect.DeepEqual:

cust1 := cutomer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(reflect.DeepEqual(cust1, cust2))

这里,即使在customer结构体中包含不可比较的类型(slice),但依然会如期望的那样进行操作并输出true。

然而,在使用reflect.DeepEqual函数的时候,有两个主要方面需要注意。

第一个方面就是该函数区分了空集合和零值

让我们比较两个customer结构体,其中一个包含nil切片,而第二个是空切片:

var cust1 interface{} = customer{id: "x"} ①
var cust2 interface{} = customer{id: "x", operations: []float64{}} ②
fmt.Print("%t\n", reflect.DeepEqual(cust1, cust2))
① Nil切片② 空切片

这段代码将打印出false,因为reflect.DeepEqual函数认为空和nil集合是不同的。这会有问题吗?当然没有。例如,如果我们想比较两个解码(unmarshaling)操作的结果,我们可能更希望提高这个差异。然而,为了有效地使用reflect.DeepEqual,有必要记住这种行为。

另一个需要关注的点在大多数语言中都是相当标准的--性能。由于此函数使用反射,因此会有性能方面的损耗。在本地使用不同大小的结构体进行一些基准测试,reflect.DeepEqual的平均执行速度要比 == 操作符慢100倍。

一般来说, == 操作符的使用场景是非常有限的。例如,它不适合用于切片和map的操作。reflect.DeepEqual能解决大多数情况下的场景。但是也有一些问题,例如性能损耗。其他一些方法也是可能的,例如实现一个自定义的比较customer的函数或方法,或在单元测试中使用像google/go-cmp或stretchr/testify这样的外部库。最后要提到的是,我们还应该注意到标准库有一些现有的比较方法。例如,如果我们想比较两个字节切片,我们可以使用bytes.Compare函数。在我们寻找外部库之前,我们应该始终确保标准库是否尚未覆盖我们的用例。