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