go 语言中的可比较类型和不可比较类型

go 语言中的可比较类型和不可比较类型

操作符变量类型
等值操作符 (==、!=)整型、浮点型、字符串、布尔型、复数、指针、管道、接口、结构体、数组
排序操作符 (<、<=、> 、 >=)整型、浮点型、字符串
不可比较类型map、slice、function

一、为什么关心可比较性

1、比较操作符

  比较操作符分为等值操作符(== 和 !=)和排序操作符(<、<=、> 、 >=),等值操作符作用的操作数必须是可比较的,排序操作符作用的操作数必须是可排序的。

2、关心可比较性的原因

  编写程序时,将变量进行比较是非常普遍的操作,因为 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())
}

1、不可比较的原因

  至于这三种类型为什么不可比较,Golang 社区没有给出官方解释,经过分析,可能是因为 比较的维度不好衡量,难以定义一种没有争议的比较规则。所以 go 官方并没有定义比较运算符(==和!=),而是只能与nil进行比较。

  比如两个 slice 类型相同、长度相同并且元素值也相同算不算相等?如果说相等,那么如果两个 slice 地址不同,还算不算相等呢?答案就可能无法统一了。至于 map 也是同样的道理。另外再看 function,两个函数实现功能一样,但实现逻辑不一样算不算相等呢?可见,这三种类型的比较容易引入歧义。

2、实现比较的方法

  使用 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 和函数)三种类型不可比较,尽管接口类型可以比较,但要警惕如果底层类型不可比较可能会引起程序崩溃的问题。

  对于类型的可排序性,即类型在面对 <、<=、> 和 >= 时的比较规则,也只有三种类型(整型、浮点型和字符串)可以比较,而且即便误用,编译器也能够帮忙拦截。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值