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

前言

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

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

可比较性

  1. 比较操作符

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

  1. 关心可比较性的原因

编写程序时,将变量进行比较是非常普遍的操作,因为 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、需要特别注意的类型

结构体类型

结构体是可以比较的,但前提是结构体成员字段全部可以比较,并且结构体成员字段类型、个数、顺序也需要相同,当结构体成员全部相等时,两个结构体相等

特别注意的点,如果结构体成员字段的顺序不相同,那么结构体也是不可以比较的。如果结构体成员字段中有不可以比较的类型,如mapslicefunction 等,那么结构体也是不可以比较的。

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 语言只有三种不可比较类型,分别是 slicemapfunction,三种类型的变量不能进行比较,只有一个例外:可以与 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,两个函数实现功能一样,但实现逻辑不一样算不算相等呢?可见,这三种类型的比较容易引入歧义。

  1. 实现比较的方法

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

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

关于我
loading