Golang Comparable 完全指南

Last Modified: 2023/08/11

概述

本文对 Go 语言中的 comparable types 作了详细的讲解,我希望这是一个完备的指南,通过阅读本文,一定能够让你对 Go 的 comparable type 有更深入的理解。

比较运算符

  • == equal
  • != not equal
  • < less
  • <= less or equal
  • > greater
  • >= greater or equal

比较运算符分为两大类:

  • ==!=,这两个运算符可用于 comparable types;
  • <<=>>= 运算符用于 ordered types;

从这里可以看出,comparable 和 ordered types 是不同的概念,一个类型是 comparable 的,那么该类型只能使用 ==!= 运算符,而不能使用排序运算符。

不论是那种类型,要想运用这些运算符都必须保证运算符的两个操作数是可以相互赋值的,也就是所谓的 assignable

下面这段代码编译是会报错,a 是 int 类型,b 的类型是 float32,a 和 b 类型不同,不可以相互赋值,因此他们也是不可以直接比较的。

var a int = 1
var b float32 = 1.0
if a == b {
}

不可比较类型

func、slice 和 map 是不可比较的类型,但这里面有个特殊情况,他们可以和 nil 字面值比较。

Go 类型比较规则

O 表示是,x 表示否。

类型 comparable ordered
bool o x
integer o o
float o o
complex o x
string o o
pointer o x
channel o x
interface o x
struct o x
array o x
类型参数 o x

注:O 不代表一定可以比较,有些类型需要满足一定条件才可以比较,后面会有详细说明。

bool 类型可比较不可排序,int 和 float 显而易见可比较可排序,对于其他类型有必要做进一步的解释如下:

1、complex type

复数类型,这种类型平时不常使用,复数包括实数部分和虚数部分,当两个值的实数部分和虚数部分均相等,则两个复数相等。

a := complex(1, 1)
b := complex(1, 1)
println(a == b) // true

2、string type

Go 语言中 string 类型可比较可排序,因此可以使用 ==!=< 等运算符。排序的依据是字典顺序,跟字符串长度没啥太大关系。

a := "a"
b := "a"
c := "c"
println(a == b) // true
println(a < c) // true
d := "abcdefg"
// 虽然 d 的长度比 c 长,但是按照字典顺序比较,d < c
println(d < c) // true

3、pointer type

如果两个指针类型指向同一个变量,或者都是 nil,那么他们是相等的。

a := "a"
b := &a
c := &a
println(b == c) // true

var d *int = nil
var e *int = nil
println(d == e) // true

这里有个特殊情况:指向两个不同的 zero-size 变量的指针,可能相等也可能不相等!

什么是 zero-size?如果一个结构体或数组类型中不包含任何字段(或元素)的大小大于零,那么它的大小为零。两个不同的大小为零的变量可能在内存中具有相同的地址。

type A struct {
	X *B
	Y *B
}
// 注意:结构体 B 为 zero-size
type B struct {
}
// 虽然 X 和 Y 指向不同的结构体实例,但是 a.X 和 a.Y 比较的输出结果为 true
a := &A{
    X: &B{},
    Y: &B{},
}
println(a.X == a.Y) // true

m := &B{}
n := &B{}
println(m == n) // false

这个例子很好的说明了 “指向两个不同的 zero-size 变量的指针,可能相等也可能不相等!”

4、channel type

可比较不可排序,两个 channel type 类型的值相等当且仅当他们由同一个 make 创建而来,或者他们都是 nil。

a := make(chan int)
b := a
println(a == b) // true

// c 和 d 不是由同一个 make 创建而来,因此 c 和 d 不相等
c := make(chan int)
d := make(chan int)
println(c == d) // false

var x chan int  = nil
var y chan int  = nil
println(x == y) // true

5、interface type

可比较不可排序。两个 interface 类型的值相等当且仅当他们的动态类型相同并且动态类型的值也相等,或者他们都是 nil。

var a int  = 1
var b float32 = 2
var c int  = 1
var a1 interface{} = a
var b1 interface{} = b
var c1 interface{} = c
println(a1 == b1) // false
println(a1 == c1) // true

var d interface{} = []int{1}
var e interface{} = 1
var f interface{} = []int{1}
println(d == e) // false
println(d == f) // panic !!!

a1 动态类型为 int,b1 的动态类型为 float,由于动态类型不同,因此 a1 == b1 为 false。
a1 和 c1 的动态类型均为 int,且值都为 1,因此 a1 == c1 为 true。
d 的动态类型为 slice,e 的动态类型为 int,由于动态类型不同,因此 d == e 为 false。
d 的动态类型为 slice,f 的动态类型为 slice,动态类型相同,但是前面说到过 slice 是不比较的,这导致 d 和 f 的比较会 panic,会 panic,会 panic !!!

比较可不是随便的事情,你以为比较的结果无非是 true 或者 false,但事实上,在 go 中,比较运用不正确会 panic,导致程序崩溃!

interface 类型的值比较有一个特殊的规则:如果两个 interface 类型的值的动态类型相同,但是他们的值是不可比较的,此时比较这两个值会 panic。

6、struct type

如果一个结构体的所有字段类型都是可比较的,那么该结构体类型是可比较的。如果两个结构体的相应非空白字段值相等,那么它们是相等的。字段按照源代码中的顺序进行比较,一旦两个字段的值不同(或所有字段已经比较完毕),比较结束。

type A struct {
	X int
}

a := A{1}
b := A{1}
c := A{2}
println(a == b) // true
println(a == c) // false

struct 类型比较也可能 panic,因为 struct 类型的比较本质上是字段的比较,如果字段包含 interface 类型,根据第五点我们知道可能会发生 panic。

type B struct {
	a interface{}
}

a := B{a: []int {1}}
b := B{a: []int {1}}
println(a == b)	// panic

7、array type

数组类型是否可比较取决于元素类型是否可比较,元素类型可比较数组才是可比较的。如果两个数组的相应元素值相等,那么它们是相等的。元素按照索引递增的顺序进行比较,一旦两个元素的值不同(或所有元素已经比较完毕),比较结束。

a := [2]interface{}{1}
b := [2]interface{}{1}
println(a == b) // true

e := [2]interface{}{2}
f := [2]interface{}{1, 2}
println(e == f) // false

c := [1]interface{}{[]int{}}
d := [1]interface{}{[]int{}}
println(c == d) // panic

8、类型参数

如果类型参数的类型集合中的所有类型都是严格可比较的,那么类型参数就是可比较的。

func Cmp[T int | float32](a, b T) {
	println(a == b)
}

func main() {
	Cmp(1, 2) // false
}

这里的类型参数 T 类型集合只包含 int 和 float32,这两个类型都是严格可比较的,因此我们对 a 和 b 进行比较。


func Cmp[T int | []int](a, b T) {
	println(a == b)
}

这里的类型参数 T 类型集合只包含 int 和 []int[]int 是 slice 类型,不可比较,因此 a == b 根本编译不通过。

严格(strictly comparable)可比较类型

一个类型是严格可比较的,如果它是可比较的且不是接口类型,也不由接口类型组成。具体规则如下:

  • bool、数值、string、pointer 和 channel 类型是严格可比较的。
  • 如果一个结构体的所有字段类型都是严格可比较的,那么结构体类型就是严格可比较的。
  • 如果数组的元素类型是严格可比较的,那么数组类型就是严格可比较的。
  • 如果类型参数的类型集合中的所有类型都是严格可比较的,那么类型参数就是严格可比较的。

总结

本文详细介绍了 go 语言中各种数据类型的比较规则,有些规则可能是违反直觉的,甚至发生 panic,所以在使用的过程中需要特别注意。

有问题吗?点此反馈!

温馨提示:反馈需要登录