深入理解Go之==
概述
相信 ==
判等操作,大家每天都在用。之前在论坛上看到不少人在问 golang ==
比较的结果。看到很多人对 golang 中 ==
的结果不太了解。确实,golang 中对 ==
的处理有一些细节的地方需要特别注意。虽然平时可能不太会遇到,但是碰到了就是大坑。本文将对 golang 中 ==
操作做一个系统的介绍。希望能对大家有所帮助。
类型
golang 中的数据类型可以分为以下 4 大类:
-
基本类型:整型(
int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune
等)、浮点数(float32/float64
)、复数类型(complex64/complex128
)、字符串(string
)。 - 复合类型(又叫聚合类型):数组和结构体类型。
- 引用类型:切片(slice)、map、channel、指针。
-
接口类型:如
error
。
==
操作最重要的一个前提是:两个操作数类型必须相同!类型必须相同!类型必须相同!
如果类型不同,那么 编译时
就会报错。
注意:
C/C++ type
为了更容易看出类型,示例代码中的变量定义都显式指定了类型。
看下面的代码:
package main import "fmt" func main() { var a int8 var b int16 // 编译错误:invalid operation a == b (mismatched types int8 and int16) fmt.Println(a == b) }
没有隐式类型转换。
package main import "fmt" func main() { type int8 myint8 var a int8 var b myint8 // 编译错误:invalid operation a == b (mismatched types int8 and myint8) fmt.Println(a == b) }
虽然myint8的底层类型是int8,但是他们是不同的类型。
下面依次通过这 4 种类型来说明 ==
是如何做比较的。
基本类型
这是最简单的一种类型。比较操作也很简单,直接比较值是否相等。没啥好说的,直接看例子。
var a uint32 = 10 var b uint32 = 20 var c uint32 = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true
有一点需要注意,浮点数的比较问题:
var a float64 = 0.1 var b float64 = 0.2 var c float64 = 0.3 fmt.Println(a + b == c) // false
因为计算机中,有些浮点数不能精确表示,浮点运算结果会有误差。如果我们分别输出 a+b
和 c
的值,会发现它们确实是不同的:
fmt.Println(a + b) fmt.Println(c) // 0.30000000000000004 // 0.3
这个问题不是 golang 独有的,只要浮点数遵循 IEEE 754 标准的编程语言都有这个问题。需要特别注意,
尽量不要做浮点数比较,确实需要比较时,计算两个浮点数的差的绝对值,如果小于一定的值就认为它们相等,比如 1e-9
。
复合类型
复合类型也叫做聚合类型。golang 中的复合类型只有两种:数组和结构体。它们是逐元素/字段比较的。
注意:数组的长度视为类型的一部分,长度不同的两个数组是不同的类型,不能直接比较。
-
对于数组来说,依次比较各个 元素
的值。根据元素类型的不同,再依据是基本类型、复合类型、引用类型或接口类型,按照特定类型的规则进行比较。所有元素全都相等,数组才是相等的。 -
对于结构体来说,依次比较各个 字段
的值。根据字段类型的不同,再依据是 4 中类型中的哪一种,按照特定类型的规则进行比较。所有字段全都相等,结构体才是相等的。
例如:
a := [4]int{1, 2, 3, 4} b := [4]int{1, 2, 3, 4} c := [4]int{1, 3, 4, 5} fmt.Println(a == b) // true fmt.Println(a == c) // false type A struct { a int b string } aa := A { a : 1, b : "test1" } bb := A { a : 1, b : "test1" } cc := A { a : 1, b : "test2" } fmt.Println(aa == bb) fmt.Println(aa == cc)
引用类型
引用类型是间接指向它所引用的数据的,保存的是数据的地址。 引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。
例如:
type A struct { a int b string } aa := &A { a : 1, b : "test1" } bb := &A { a : 1, b : "test1" } cc := aa fmt.Println(aa == bb) fmt.Println(aa == cc)
因为 aa
和 bb
指向的两个不同的结构体,虽然它们指向的值是相等的(见上面复合类型的比较),但是它们不等。
aa
和 cc
指向相同的结构体,所以它们相等。
再看看 channel
的比较:
ch1 := make(chan int, 1) ch2 := make(chan int, 1) ch3 := ch1 fmt.Println(ch1 == ch2) fmt.Println(ch1 == ch3)
ch1
和 ch2
虽然类型相同,但是指向不同的 channel
,所以它们不等。
ch1
和 ch3
指向相同的 channel
,所以它们相等。
关于引用类型,有两个比较特殊的规定:
-
切片之间不允许比较。切片只能与
nil
值比较。 -
map
之间不允许比较。map
只能与nil
值比较。
为什么要做这样的规定?我们先来说切片。因为切片是引用类型,它可以间接的指向自己。例如:
a := []interface{}{ 1, 2.0 } a[1] = a fmt.Println(a) // !!! // runtime: goroutine stack exceeds 1000000000-byte limit // fatal error: stack overflow
上面代码将 a
赋值给 a[1]
导致递归引用, fmt.Println(a)
语句直接爆栈。
- 切片如果直接比较引用地址,是不合适的。首先,切片与数组是比较相近的类型,比较方式的差异会造成使用者的混淆。另外,长度和容量是切片类型的一部分,不同长度和容量的切片如何比较?
- 切片如果像数组那样比较里面的元素,又会出现上来提到的循环引用的问题。虽然可以在语言层面解决这个问题,但是 golang 团队认为不值得为此耗费精力。
基于上面两点原因,golang 直接规定 切片类型不可比较
。使用 ==
比较切片直接编译报错。
例如:
var a []int var b []int // invalid operation: a == b (slice can only be compared to nil) fmt.Println(a == b)
错误信息很明确。
因为 map
的值类型可能为不可比较类型(见下面,切片是不可比较类型),所以 map
类型也不可比较 。
接口类型
接口类型是 golang 中比较重要的一种类型。接口类型的值,我们称为接口值。一个接口值是由两个部分组成的,具体类型(即该接口存储的值的类型)和该类型的一个值。引用《go 程序设计语言》的名称,分别称为 动态类型
和 动态值
。接口值的比较涉及这两部分的比较,只有当 动态类型完全相同
且动态值相等(动态值使用 ==
比较),两个接口值才是相等的。
例如:
var a interface{} = 1 var b interface{} = 2 var c interface{} = 1 var d interface{} = 1.0 fmt.Println(a == b) // false fmt.Println(a == c) // true fmt.Println(a == d) // false
a
和 b
动态类型相同(都是 int
),动态值也相同(都是 1
,基本类型比较),故两者相等。
a
和 c
动态类型相同,动态值不等(分别为 1
和 2
,基本类型比较),故两者不等。
a
和 d
动态类型不同, a
为 int
, d
为 float64
,故两者不等。
type A struct { a int b string } var aa interface{} = A { a: 1, b: "test" } var bb interface{} = A { a: 1, b: "test" } var cc interface{} = A { a: 2, b: "test" } fmt.Println(aa == bb) // true fmt.Println(aa == cc) // false var dd interface{} = &A { a: 1, b: "test" } var ee interface{} = &A { a: 1, b: "test" } fmt.Println(dd == ee) // false
aa
和 bb
动态类型相同(都是 A
),动态值也相同(结构体 A
,见上面复合类型的比较规则),故两者相等。
aa
和 cc
动态类型相同,动态值不同,故两者不等。
dd
和 ee
动态类型相同(都是 *A
),动态值使用指针(引用)类型的比较,由于不是指向同一个地址,故不等。
注意:
如果接口的动态值不可比较,强行比较会 panic
!!!
var a interface{} = []int{1, 2, 3, 4} var b interface{} = []int{1, 2, 3, 4} // panic: runtime error: comparing uncomparable type []int fmt.Println(a == b)
a
和 b
的动态值是切片类型,而切片类型不可比较,所以 a == b
会 panic
。
接口值的比较不要求接口类型(注意不是动态类型)完全相同,只要一个接口可以转化为另一个就可以比较。例如:
var f *os.File var r io.Reader = f var rc io.ReadCloser = f fmt.Println(r == rc) // true var w io.Writer = f // invalid operation: r == w (mismatched types io.Reader and io.Writer) fmt.Println(r == w)
r
的类型为 io.Reader
接口, rc
的类型为 io.ReadCloser
接口。查看源码, io.ReadCloser
的定义如下:
type ReadCloser interface { Reader Closer }
io.ReadCloser
可转化为 io.Reader
,故两者可比较。
而 io.Writer
不可转化为 io.Reader
,编译报错。
使用 type
定义的类型
使用 type
可以基于现有类型定义新的类型。新类型会根据它们的底层类型来比较。例如:
type myint int var a myint = 10 var b myint = 20 var c myint = 10 fmt.Println(a == b) // false fmt.Println(a == c) // true type arr4 [4]int var aa arr4 = [4]int{1, 2, 3, 4} var bb arr4 = [4]int{1, 2, 3, 4} var cc arr4 = [4]int{1, 2, 3, 5} fmt.Println(aa == bb) fmt.Println(aa == cc)
myint
根据底层类型 int
来比较。
arr4
根据底层类型 [4]int
来比较。
不可比较性
前面说过,golang 中的切片类型是不可比较的。所有含有切片的类型都是不可比较的。例如:
- 数组元素是切片类型。
- 结构体有切片类型的字段。
- 指针指向的是切片类型。
不可比较性会传递,如果一个结构体由于含有切片字段不可比较,那么将它作为元素的数组不可比较,将它作为字段类型的结构体不可比较。
谈谈 map
由于 map
的 key
是使用 ==
来判等的,所以所有不可比较的类型都不能作为 map
的 key
。例如:
// invalid map key type []int m1 := make(map[[]int]int) type A struct { a []int b string } // invalid map key type A m2 := make(map[A]int)
由于切片类型不可比较,不能作为 map
的 key
,编译时 m1 := make(map[[]int]int)
报错。
由于结构体 A
含有切片字段,不可比较,不能作为 map
的 key
,编译报错。
总结
本文详尽介绍了 golang 中 ==
操作的细节,希望能对大家有所帮助。