绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
深入理解Go之==
2020-04-26 09:35:08

概述

相信 ==判等操作,大家每天都在用。之前在论坛上看到不少人在问 golang ==比较的结果。看到很多人对 golang 中 ==的结果不太了解。确实,golang 中对 ==的处理有一些细节的地方需要特别注意。虽然平时可能不太会遇到,但是碰到了就是大坑。本文将对 golang 中 ==操作做一个系统的介绍。希望能对大家有所帮助。

类型

golang 中的数据类型可以分为以下 4 大类:

  1. 基本类型:整型( int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune等)、浮点数( float32/float64)、复数类型( complex64/complex128)、字符串( string)。

  2. 复合类型(又叫聚合类型):数组和结构体类型。

  3. 引用类型:切片(slice)、map、channel、指针。

  4. 接口类型:如 error

==操作重要的一个前提是:两个操作数类型必须相同!类型必须相同!类型必须相同!

如果类型不同,那么编译时就会报错。

注意:

  1. golang 的类型系统非常严格,没有 C/C++中的隐式类型转换。虽然写起来稍微有些麻烦,但是能避免今后非常多的麻烦!!!

  2. 在 golang 中可以通过 type定义新类型。新定义的类型与底层类型不同,不能直接比较。

为了更容易看出类型,示例代码中的变量定义都显式指定了类型。

看下面的代码:

  1. package main


  2. import "fmt"


  3. func main() {

  4. var a int8

  5. var b int16

  6. // 编译错误:invalid operation a == b (mismatched * int8 and int16)

  7. fmt.Println(a == b)

  8. }

没有隐式类型转换。

  1. package main


  2. import "fmt"


  3. func main() {

  4. type int8 myint8

  5. var a int8

  6. var b myint8

  7. // 编译错误:invalid operation a == b (mismatched * int8 and myint8)

  8. fmt.Println(a == b)

  9. }

虽然myint8的底层类型是int8,但是他们是不同的类型。

下面依次通过这 4 种类型来说明 ==是如何做比较的。

基本类型

这是简单的一种类型。比较操作也很简单,直接比较值是否相等。没啥好说的,直接看例子。

  1. var a uint32 = 10

  2. var b uint32 = 20

  3. var c uint32 = 10

  4. fmt.Println(a == b) // false

  5. fmt.Println(a == c) // true

有一点需要注意,浮点数的比较问题:

  1. var a float64 = 0.1

  2. var b float64 = 0.2

  3. var c float64 = 0.3

  4. fmt.Println(a + b == c) // false

因为计算机中,有些浮点数不能表示,浮点运算结果会有误差。如果我们分别输出 a+b和 c的值,会发现它们确实是不同的:

  1. fmt.Println(a + b)

  2. fmt.Println(c)


  3. // 0.30000000000000004

  4. // 0.3

这个问题不是 golang 独有的,只要浮点数遵循 IEEE 754 标准的编程语言都有这个问题。需要特别注意,尽量不要做浮点数比较,确实需要比较时,计算两个浮点数的差的值,如果小于一定的值就认为它们相等,比如 1e-9

复合类型

复合类型也叫做聚合类型。golang 中的复合类型只有两种:数组和结构体。它们是逐元素/字段比较的。

注意:数组的长度视为类型的一部分,长度不同的两个数组是不同的类型,不能直接比较

  • 对于数组来说,依次比较各个元素的值。根据元素类型的不同,再依据是基本类型、复合类型、引用类型或接口类型,按照特定类型的规则进行比较。所有元素全都相等,数组才是相等的。

  • 对于结构体来说,依次比较各个字段的值。根据字段类型的不同,再依据是 4 中类型中的哪一种,按照特定类型的规则进行比较。所有字段全都相等,结构体才是相等的。

例如:

  1. a := [4]int{1, 2, 3, 4}

  2. b := [4]int{1, 2, 3, 4}

  3. c := [4]int{1, 3, 4, 5}

  4. fmt.Println(a == b) // true

  5. fmt.Println(a == c) // false


  6. type A struct {

  7. a int

  8. b string

  9. }

  10. aa := A { a : 1, b : "test1" }

  11. bb := A { a : 1, b : "test1" }

  12. cc := A { a : 1, b : "test2" }

  13. fmt.Println(aa == bb)

  14. fmt.Println(aa == cc)

引用类型

引用类型是间接指向它所引用的数据的,保存的是数据的地址。引用类型的比较实际判断的是两个变量是不是指向同一份数据,它不会去比较实际指向的数据。

例如:

  1. type A struct {

  2. a int

  3. b string

  4. }


  5. aa := &A { a : 1, b : "test1" }

  6. bb := &A { a : 1, b : "test1" }

  7. cc := aa

  8. fmt.Println(aa == bb)

  9. fmt.Println(aa == cc)

因为 aa和 bb指向的两个不同的结构体,虽然它们指向的值是相等的(见上面复合类型的比较),但是它们不等。 aa和 cc指向相同的结构体,所以它们相等。

再看看 channel的比较:

  1. ch1 := make(chan int, 1)

  2. ch2 := make(chan int, 1)

  3. ch3 := ch1


  4. fmt.Println(ch1 == ch2)

  5. fmt.Println(ch1 == ch3)

ch1和 ch2虽然类型相同,但是指向不同的 channel,所以它们不等。 ch1和 ch3指向相同的 channel,所以它们相等。

关于引用类型,有两个比较特殊的规定:

  • 切片之间不允许比较。切片只能与 nil值比较。

  • map之间不允许比较。 map只能与 nil值比较。

为什么要做这样的规定?我们先来说切片。因为切片是引用类型,它可以间接的指向自己。例如:

  1. a := []interface{}{ 1, 2.0 }

  2. a[1] = a

  3. fmt.Println(a)


  4. // !!!

  5. // runtime: goroutine stack exceeds 1000000000-byte limit

  6. // fatal error: stack overflow

上面代码将 a赋值给 a[1]导致递归引用, fmt.Println(a)语句直接爆栈。

  • 切片如果直接比较引用地址,是不合适的。首先,切片与数组是比较相近的类型,比较方式的差异会造成使用者的混淆。另外,长度和容量是切片类型的一部分,不同长度和容量的切片如何比较?

  • 切片如果像数组那样比较里面的元素,又会出现上来提到的循环引用的问题。虽然可以在语言层面解决这个问题,但是 golang 团队认为不值得为此耗费精力。

基于上面两点原因,golang 直接规定切片类型不可比较。使用 ==比较切片直接编译报错。

例如:

  1. var a []int

  2. var b []int


  3. // invalid operation: a == b (slice can only be compared to nil)

  4. fmt.Println(a == b)

错误信息很明确。

因为 map的值类型可能为不可比较类型(见下面,切片是不可比较类型),所以 map类型也不可比较🤣。

接口类型

接口类型是 golang 中比较重要的一种类型。接口类型的值,我们称为接口值。一个接口值是由两个部分组成的,具体类型(即该接口存储的值的类型)和该类型的一个值。引用《go 程序设计语言》的名称,分别称为动态类型动态值。接口值的比较涉及这两部分的比较,只有当动态类型完全相同且动态值相等(动态值使用 ==比较),两个接口值才是相等的。

例如:

  1. var a interface{} = 1

  2. var b interface{} = 2

  3. var c interface{} = 1

  4. var d interface{} = 1.0

  5. fmt.Println(a == b) // false

  6. fmt.Println(a == c) // true

  7. fmt.Println(a == d) // false

a和 b动态类型相同(都是 int),动态值也相同(都是 1,基本类型比较),故两者相等。 a和 c动态类型相同,动态值不等(分别为 1和 2,基本类型比较),故两者不等。 a和 d动态类型不同, a为 int, d为 float64,故两者不等。

  1. type A struct {

  2. a int

  3. b string

  4. }


  5. var aa interface{} = A { a: 1, b: "test" }

  6. var bb interface{} = A { a: 1, b: "test" }

  7. var cc interface{} = A { a: 2, b: "test" }


  8. fmt.Println(aa == bb) // true

  9. fmt.Println(aa == cc) // false


  10. var dd interface{} = &A { a: 1, b: "test" }

  11. var ee interface{} = &A { a: 1, b: "test" }

  12. fmt.Println(dd == ee) // false

aa和 bb动态类型相同(都是 A),动态值也相同(结构体 A,见上面复合类型的比较规则),故两者相等。 aa和 cc动态类型相同,动态值不同,故两者不等。 dd和 ee动态类型相同(都是 *A),动态值使用指针(引用)类型的比较,由于不是指向同一个地址,故不等。

注意:

如果接口的动态值不可比较,强行比较会 panic!!!

  1. var a interface{} = []int{1, 2, 3, 4}

  2. var b interface{} = []int{1, 2, 3, 4}

  3. // panic: runtime error: comparing uncomparable type []int

  4. fmt.Println(a == b)

a和 b的动态值是切片类型,而切片类型不可比较,所以 a==b会 panic

接口值的比较不要求接口类型(注意不是动态类型)完全相同,只要一个接口可以转化为另一个就可以比较。例如:

  1. var f *os.File


  2. var r io.Reader = f

  3. var rc io.ReadCloser = f

  4. fmt.Println(r == rc) // true


  5. var w io.Writer = f

  6. // invalid operation: r == w (mismatched * io.Reader and io.Writer)

  7. fmt.Println(r == w)

r的类型为 io.Reader接口, rc的类型为 io.ReadCloser接口。查看源码, io.ReadCloser的定义如下:

  1. type ReadCloser interface {

  2. Reader

  3. Closer

  4. }

io.ReadCloser可转化为 io.Reader,故两者可比较。

而 io.Writer不可转化为 io.Reader,编译报错。

使用 type定义的类型

使用 type可以基于现有类型定义新的类型。新类型会根据它们的底层类型来比较。例如:

  1. type myint int

  2. var a myint = 10

  3. var b myint = 20

  4. var c myint = 10

  5. fmt.Println(a == b) // false

  6. fmt.Println(a == c) // true


  7. type arr4 [4]int

  8. var aa arr4 = [4]int{1, 2, 3, 4}

  9. var bb arr4 = [4]int{1, 2, 3, 4}

  10. var cc arr4 = [4]int{1, 2, 3, 5}

  11. fmt.Println(aa == bb)

  12. fmt.Println(aa == cc)

myint根据底层类型 int来比较。 arr4根据底层类型 [4]int来比较。

不可比较性

前面说过,golang 中的切片类型是不可比较的。所有含有切片的类型都是不可比较的。例如:

  • 数组元素是切片类型。

  • 结构体有切片类型的字段。

  • 指针指向的是切片类型。

不可比较性会传递,如果一个结构体由于含有切片字段不可比较,那么将它作为元素的数组不可比较,将它作为字段类型的结构体不可比较

谈谈 map

由于 map的 key是使用 ==来判等的,所以所有不可比较的类型都不能作为 map的 key。例如:

  1. // invalid map key type []int

  2. m1 := make(map[[]int]int)


  3. type A struct {

  4. a []int

  5. b string

  6. }

  7. // invalid map key type A

  8. m2 := make(map[A]int)

由于切片类型不可比较,不能作为 map的 key,编译时 m1:=make(map[[]int]int)报错。由于结构体 A含有切片字段,不可比较,不能作为 map的 key,编译报错。

总结

本文详尽介绍了 golang 中 ==操作的细节,希望能对大家有所帮助。

参考

  1. Go程序设计语言


分享好友

分享这个小栈给你的朋友们,一起进步吧。

Go 每日一库
创建时间:2020-04-24 14:01:14
每天学习一个 Go 语言库~
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • darjun
    栈主

小栈成员

查看更多
  • ?
  • 小雨滴
  • 飘絮絮絮丶
  • 栈栈
戳我,来吐槽~