深刻理解Go之==

概述

相信==判等操做,你們天天都在用。以前在論壇上看到很多人在問 golang ==比較的結果。看到不少人對 golang 中==的結果不太瞭解。確實,golang 中對==的處理有一些細節的地方須要特別注意。雖然平時可能不太會遇到,可是碰到了就是大坑。本文將對 golang 中==操做作一個系統的介紹。但願能對你們有所幫助。git

類型

golang 中的數據類型能夠分爲如下 4 大類:github

  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

==操做最重要的一個前提是:兩個操做數類型必須相同!類型必須相同!類型必須相同!golang

若是類型不一樣,那麼編譯時就會報錯。編程

注意:數組

  1. golang 的類型系統很是嚴格,沒有C/C++中的隱式類型轉換。雖然寫起來稍微有些麻煩,可是能避免從此很是多的麻煩!!!
  2. golang 中能夠經過type定義新類型。新定義的類型與底層類型不一樣,不能直接比較。

爲了更容易看出類型,示例代碼中的變量定義都顯式指定了類型。bash

看下面的代碼:編程語言

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)
}
複製代碼

沒有隱式類型轉換。ui

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,可是他們是不一樣的類型。spa

下面依次經過這 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+bc的值,會發現它們確實是不一樣的:

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)
複製代碼

由於aabb指向的兩個不一樣的結構體,雖然它們指向的值是相等的(見上面複合類型的比較),可是它們不等。 aacc指向相同的結構體,因此它們相等。

再看看channel的比較:

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch3 := ch1

fmt.Println(ch1 == ch2)
fmt.Println(ch1 == ch3)
複製代碼

ch1ch2雖然類型相同,可是指向不一樣的channel,因此它們不等。 ch1ch3指向相同的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
複製代碼

ab動態類型相同(都是int),動態值也相同(都是1,基本類型比較),故二者相等。 ac動態類型相同,動態值不等(分別爲12,基本類型比較),故二者不等。 ad動態類型不一樣,aintdfloat64,故二者不等。

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
複製代碼

aabb動態類型相同(都是A),動態值也相同(結構體A,見上面複合類型的比較規則),故二者相等。 aacc動態類型相同,動態值不一樣,故二者不等。 ddee動態類型相同(都是*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)
複製代碼

ab的動態值是切片類型,而切片類型不可比較,因此a == bpanic

接口值的比較不要求接口類型(注意不是動態類型)徹底相同,只要一個接口能夠轉化爲另外一個就能夠比較。例如:

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

因爲mapkey是使用==來判等的,因此全部不可比較的類型都不能做爲mapkey。例如:

// 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)
複製代碼

因爲切片類型不可比較,不能做爲mapkey,編譯時m1 := make(map[[]int]int)報錯。 因爲結構體A含有切片字段,不可比較,不能做爲mapkey,編譯報錯。

總結

本文詳盡介紹了 golang 中==操做的細節,但願能對你們有所幫助。

參考

  1. Go程序設計語言

個人我的主頁

相關文章
相關標籤/搜索