Go语言中的可比较类型和不可比较类型
前言
go 语言中的可比较类型和不可比较类型
操作符 | 变量类型 |
---|---|
等值操作符 (==、!=) | 整型、浮点型、字符串、布尔型、复数、指针、管道、接口、结构体、数组 |
排序操作符 (<、<=、> 、 >=) | 整型、浮点型、字符串 |
不可比较类型 | map、slice、function |
可比较性
- 比较操作符
比较操作符分为等值操作符(== 和 !=)和排序操作符(<、<=、> 、 >=),等值操作符作用的操作数必须是可比较的,排序操作符作用的操作数必须是可排序的。
- 关心可比较性的原因
编写程序时,将变量进行比较是非常普遍的操作,因为 Go 语言对某些类型的比较做了严格的限制,如果错误的将两个变量进行比较,编译器有可能无法识别,进而产生运行时错误,即 panic。
有些无法比较的,编译器能够拦截,例如使用排序操作符时,如果两个变量是无法比较的(不能分出大小),编译器能够给出错误提示。但对于等值操作符,如果两个操作数无法比较,编译器则不能保证一定能够识别,例如接口类型的变量比较时,编译器不会给出错误提示,只有运行时才能够判断是否可比较,此时有错误就会引起程序崩溃。所以了解类型的可比较性就非常重要了。
可比较类型
Go 语言中可以进行等值比较的类型包括布尔类型、整型、浮点型、字符串类型、复数类型、指针类型、管道类型、接口类型、结构体类型和数组类型。其中布尔类型、整型比较常见且易于理解,在此不做过多赘述,下面着重介绍其他类型,因为它们或多或少都有值得注意的地方。
1、正常的类型
浮点型
Go 语言中两个浮点型变量可以直接比较,Go 语言的浮点型参照的是 IEEE(电气与电子工程师协会)754 号标准,也即二进制浮点数算术标准,两个符点数在一定精度下无法区分大小即相等。这个标准并不是被所有编程语言所采用,比如 C 语言,它诞生时间比这个标准还要早,在 C 语言的实现中浮点数是不能进行等值比较的。
f1 := 0.10
f2 := 0.1
fmt.Println(f1 == f2) // 输出 true
fmt.Println(f1 < f2) // 输出 false
字符串类型
string 类型比较是按字节逐个比较的,当两个 string 变量所有字节值都相等时,两个 string 变量则相等。做等值比较时,第一个字节不相等,则直接判定为不相等;做排序比较时,第一个字节值小于,则直接判定为小于。
s1 := "中" // Unicode 编码占 3 个字节:228,184,173
s2 := "国" // Unicode 编码占 3 个字节:229,155,189
fmt.Println(s1 == s2) // 输出 false
fmt.Println(s1 < s2) // 输出 true
复数类型
复数类型是可以比较的,复数类型的实部和虚部由浮点数进行表示,只有当两个复数实部和虚部都相等才相等。
c1 := complex(1, 2)
c2 := complex(1, 2)
c3 := complex(1, 3)
fmt.Println(c1 == c2) // 输出 true
fmt.Println(c1 == c3) // 输出 false
fmt.Println(c1 < c2)
// 错误提示:Invalid operation: c1 < c2 (the operator < is not defined on complex128)
指针类型
指针本质上是个整型,其值反映的是内存地址,因此指针类型是可以比较的,两个指针变量如果指向的地址相同(或同为 nil)则认为相等,否则不相等,两个指针相等则代表指向同一块内存。
str1 := "Hello World"
p1 := &str1
str2 := "Hello World"
p2 := &str2
fmt.Println(p1 == p2) // 输出 false
fmt.Println(p1 < p2)
// 错误提示:Invalid operation: p1 < p2 (the operator < is not defined on *string)
管道类型
管道是可以比较的,管道本质上是个指针,make 语句生成的是一个管道的指针,所以管道的比较规则与指针相同,两个管道变量如果是同一个 make 语句声明(或同为 nil)则两个管道相等,否则不等。
cha := make(chan int, 10)
chb := make(chan int, 10)
chc := cha
fmt.Println(cha == chb) // 输出 false
fmt.Println(cha == chc) // 输出 true
// 管道 cha 和 chb 虽然类型和空间完全相同,但由于出自不同的 make 语句,所以两个管道不相等
// 但管道 chc 由于获得了管道 cha 的地址,所以管道 cha 和 chc 相等
fmt.Println(cha < chc)
// 错误提示:Invalid operation: cha < chc (the operator < is not defined on chan int)
数组类型
数组类型是可以比较的,如果两个数组类型(元素类型和声明长度)相同、每个元素均相同,则两个数组相等,否则不等。
arr1 := [10]int{1, 2, 3}
arr2 := [10]int{1, 2}
fmt.Println(arr1 == arr2) // 输出 false
arr2[2] = 3
fmt.Println(arr1 == arr2) // 输出 true
fmt.Println(arr1 < arr2)
// 错误提示:Invalid operation: arr1 < arr2 (the operator < is not defined on [10]int)
2、需要特别注意的类型
结构体类型
结构体是可以比较的,但前提是结构体成员字段全部可以比较,并且结构体成员字段类型、个数、顺序也需要相同,当结构体成员全部相等时,两个结构体相等。
特别注意的点,如果结构体成员字段的顺序不相同,那么结构体也是不可以比较的。如果结构体成员字段中有不可以比较的类型,如map
、slice
、function
等,那么结构体也是不可以比较的。
func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "Zhang San"}
sn2 := struct {
age int
name string
}{age: 11, name: "Zhang San"}
fmt.Println(sn1 == sn2) // 输出 true
sn3 := struct {
name string
age int
}{age: 11, name: "Zhang San"}
fmt.Println(sn1 == sn3)
// 错误提示:Invalid operation: sn1 == sn3 (mismatched types struct {...} and struct {...})
sn4 := struct {
name string
age int
grade map[string]int
}{age: 11, name: "Zhang San"}
sn5 := struct {
name string
age int
grade map[string]int
}{age: 11, name: "Zhang San"}
fmt.Println(sn4 == sn5)
// 错误提示:Invalid operation: sn4 == sn5 (the operator == is not defined on struct {...})
}
接口类型
Go 语言中,接口(interface)是对非接口值(例如指针,struct 等)的封装,内部实现包含了 2 个字段,类型 T 和 值 V。
type iface struct {
tab *itab // 保存变量类型(以及方法集)
data unsafe.Pointer // 变量值位于堆栈的指针
}
接口是由 struct 表示的,所谓的底层类型即 iface.tab
字段,而底层值即 iface.data
,因此接口类型的比较就演变成了结构体比较。
两个接口类型比较时,会先比较 T,再比较 V。接口类型与非接口类型比较时,会先将非接口类型尝试转换为接口类型,再按接口比较的规则进行比较。如果两个接口变量底层类型和值完全相同(或同为 nil
)则两个变量相等,否则不等。
接口类型比较时,如果底层类型不可比较,则会发生 panic
。
package main
import "fmt"
type Animal interface {
Speak() string
}
type Duck struct {
Name string
}
func (a Duck) Speak() string {
return "I'm " + a.Name
}
type Cat struct {
Name string
}
func (a Cat) Speak() string {
return "I'm " + a.Name
}
type Bird struct {
Name string
SpeakFunc func() string
}
func (a Bird) Speak() string {
return "I'm " + a.SpeakFunc()
}
// Animal 为接口类型,Duck 和 Cat 分别实现了该接口。
func main() {
var d1, d2, c1 Animal
d1 = Duck{Name: "Donald Duck"}
d2 = Duck{Name: "Donald Duck"}
c1 = Cat{Name: "Donald Duck"}
fmt.Println(d1 == d2) // 输出 true
fmt.Println(d1 == c1) // 输出 false
// 接口变量 d1、d2 底层类型同为 Duck 并且底层值相同,所以 d1 和 d2 相等。
// 接口变量 c1 底层类型为 Cat,尽管底层值相同,但类型不同,c1 与 d1 也不相等。
var animal Animal
animal = Duck{Name: "Donald Duck"}
var duck Duck
duck = Duck{Name: "Donald Duck"}
fmt.Println(animal == duck) // 输出 true
// 当 struct 和接口进行比较时,可以简单地把 struct 转换成接口然后再按接口比较的规则进行判定。
// animal 为接口变量,而 duck 为 struct 变量,底层类型同为 Duck 并且底层值相同,二者判定为相等。
var b1 Animal = Bird{
Name: "bird",
SpeakFunc: func() string {
return "I'm Poly"
}}
var b2 Animal = Bird{
Name: "bird",
SpeakFunc: func() string {
return "I'm eagle"
}}
fmt.Println(b1 == b2)
// panic: runtime error: comparing uncomparable type main.Bird
// 结构体 Bird 也实现了 Animal 接口,但结构体中增加了一个不可比较的函数类型成员 SpeakFunc,
// 因此 Bird 变成了不可比较类型,接口类型变量 b1 和 b2 底层类型为 Bird,在比较时会触发 panic。
}
nil 类型
2 个 nil 类型可能不相等,两个nil 只有在类型相同时才相等。例如,interface 在运行时绑定值,只有值为 nil 接口值才为 nil,但是与指针的 nil 不相等。
func main() {
var p *int = nil
var i interface{}
fmt.Println(p == nil) // 输出 true
fmt.Println(i == nil) // 输出 true
fmt.Println(i == p) // 输出 false
}
不可比较类型
Go 语言只有三种不可比较类型,分别是 slice
、map
、function
,三种类型的变量不能进行比较,只有一个例外:可以与 nil
进行比较。
func main() {
aSlice := make([]string, 0)
bSlice := make([]string, 0)
aMap := make(map[string]int)
bMap := make(map[string]int)
aFunc := func() {}
bFunc := func() {}
fmt.Println(aSlice == bSlice)
// 错误提示:Invalid operation: aSlice == bSlice (the operator == is not defined on []string)
fmt.Println(aMap == bMap)
// 错误提示:Invalid operation: aMap == bMap (the operator == is not defined on map[string]int)
fmt.Println(aFunc == bFunc)
// 错误提示:Invalid operation: aFunc == bFunc (the operator == is not defined on func())
}
- 不可比较的原因
至于这三种类型为什么不可比较,Golang 社区没有给出官方解释,经过分析,可能是因为 比较的维度不好衡量,难以定义一种没有争议的比较规则。所以 go 官方并没有定义比较运算符(==
和!=
),而是只能与nil
进行比较。
比如两个 slice 类型相同、长度相同并且元素值也相同算不算相等?如果说相等,那么如果两个 slice 地址不同,还算不算相等呢?答案就可能无法统一了。至于 map 也是同样的道理。另外再看 function,两个函数实现功能一样,但实现逻辑不一样算不算相等呢?可见,这三种类型的比较容易引入歧义。
- 实现比较的方法
使用 reflect.TypeOf(value).Comparable()
判断可否进行比较, 使用 reflect.DeepEqual(value 1, value 2)
进行比较,当然也有特殊情况,例如 []byte
,通过 bytes.Equal
函数进行比较。但是反射非常影响性能。
func main() {
s := "Hello World"
aMap := make(map[string]int)
bMap := make(map[string]int)
fmt.Println(reflect.TypeOf(s).Comparable()) // 输出 true
fmt.Println(reflect.TypeOf(aMap).Comparable()) // 输出 false
fmt.Println(reflect.TypeOf(bMap).Comparable()) // 输出 false
fmt.Println(reflect.DeepEqual(aMap, bMap)) // 输出 true
aMap["s"] = 1
fmt.Println(reflect.DeepEqual(aMap, bMap)) // 输出 false
}
总结
本文主要介绍了 go 语言中类型的可比较性,即各种类型在面对 == 和 != 操作符的比较规则,只有三种类型(切片、map 和函数)三种类型不可比较,尽管接口类型可以比较,但要警惕如果底层类型不可比较可能会引起程序崩溃的问题。
对于类型的可排序性,即类型在面对 <、<=、> 和 >= 时的比较规则,也只有三种类型(整型、浮点型和字符串)可以比较,而且即便误用,编译器也能够帮忙拦截。