寫這篇文章的時候,已經離我找工作有一段時間了,但是覺得這道題不管是面試還是日常的工作中,都會經常遇到,所以還是特意寫一篇文章,記錄下自己對Golang中`==`的理解。如文章中出現不對的地方,請不吝賜教,謝謝。 > 註意,以下文章內容是基於 go1.16.4 進行演示的,如果和你驗證時,結果不一致, ...
寫這篇文章的時候,已經離我找工作有一段時間了,但是覺得這道題不管是面試還是日常的工作中,都會經常遇到,所以還是特意寫一篇文章,記錄下自己對Golang中==
的理解。如文章中出現不對的地方,請不吝賜教,謝謝。
註意,以下文章內容是基於 go1.16.4 進行演示的,如果和你驗證時,結果不一致,可能 Go 的判斷規則有所改變。
1、面試題
大家可以先不看結果,想想答案,再看後面的結果以及相關的分析。
type T interface {
}
func main() {
var (
t T
p *T
i1 interface{} = t
i2 interface{} = p
)
fmt.Println(i1 ==t, i1 == nil)
fmt.Println(i2 ==p, i2 == nil)
fmt.Println(t == nil)
fmt.Println(p == nil)
}
執行結果:
true true
true false
true
true
分析:
1、interface 值由動態類型
和動態值
組成。只有在類型
和值
都相同時才相等。介面變數i1
是介面類型的零值,也就是它的類型和值部分都是nil
,介面變數i2
的動態值雖然是零值,但是動態類型為 *T
。
2、變數 t、p 都沒有初始化,未分配記憶體,所以 變數t、p 都等於 nil。
對於上面的描述不太清楚的同學,不用著急,我們一起來學習 Golang 中的
==
,有較為詳細的介紹。
2、Golang中的數據類型
Golang中的數據類型分為4大類
,他們分別是:
- 基本類型 (Primary types): 整型(
int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune
等)、浮點數(float32/float64
)、複數類型(complex64/complex128
)、字元串(string
)、布爾(true/false)。這些是Go語言內置的基本數據類型,它們是Go語言的原始數據類型,不能再細分。 - 複合類型 (Composite types):又叫聚合類型。包括
數組、結構體
。複合類型允許將多個值組合成一個新的數據結構。 - 引用類型 (Reference types):這些類型在記憶體中存儲的是數據的地址,包括
指針、切片(slice)、映射(map)、通道(channel)、函數類型(func)
。引用類型允許在函數間共用和修改數據。 - 介面類型 (Interface types):介面類型是一種抽象類型,它定義了對象的行為,而不關心對象的具體類型。通過實現介面,可以實現多態性和代碼復用。比如
error
。
其實介面類型可以看作是引用類型,在 Go 中,介面類型是一種特殊的引用類型,它包含一個指向實際數據的指針以及類型信息。當你將一個具體類型的值賦給介面變數時,介面會存儲一個指向實際數據的指針或實際數據的拷貝。因此,介面可以看作是對其他類型的引用,而不是直接包含實際數據。
在Go語言中,自定義類型屬於基本類型
的概念中。
自定義類型屬於基本類型的一種,它通過使用 type 關鍵字來創建新的類型,底層使用基本數據類型。通過自定義類型,我們可以為基本類型賦予更多語義,並且可以為它們定義自己的方法。自定義類型和其他基本類型具有相同的操作和運算規則,但在類型系統中它們是不同的類型。
例如使用
type number int64
時,我們自定義了一種數據類型,叫做number
。雖然它底層使用了int64
,但在類型系統中,number
和float64
是不同的類型。
在Go語言中,還有一種類型別名
的叫法,是 Go1.9 引用的新功能。
類型別名規定:TypeAlias只是Type的別名,本質上TypeAlias與Type是同一個類型。
例如:
type byte = uint8
type rune = int32
==
操作最重要的一個前提是:兩個操作數類型必須相同!!!
golang 的類型系統非常嚴格,沒有
C/C++/python
中的隱式類型轉換。這個需要註意。
3、四大類型如何使用 ==
3.1、基本類型
基本類型的比較,就比較簡單直觀,直接使用==
判斷就好了,註意的是Go中並沒有隱式轉換,而且類型一致才可以
。
package main
import "fmt"
func main() {
var a int64
var b int64
var c int32
fmt.Println(a == b)
fmt.Println(c)
// Invalid operation: a == c (mismatched types int64 and int32)
//fmt.Println(a == c)
}
接下來我們看看浮點數的比較:
package main
import "fmt"
func main() {
var a float64 = 0.1
var b float64 = 0.2
var c float64 = 0.3
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(a+b == c) // false
}
是不是有點小驚訝,這個是因為Go 中的 浮點數遵循 IEEE 754 標準,所以會有有些浮點數不能精確表示,浮點運算結果會有誤差。
想大概瞭解電腦是如何表示浮點數的可以看看下麵的文章,有一個基礎的瞭解。
註意:
浮點數做 判等 操作一般是使用 計算兩個浮點數的差的絕對值,如果小於一定的值就認為它們相等,比如
1e-9
。
package main
import (
"fmt"
"math"
)
func main() {
var a = 0.1
var b = 0.2
var c = 0.3
fmt.Println(a + b) // 0.30000000000000004
fmt.Println(math.Abs((a+b)-c) < 1e-9) // true
fmt.Printf("%T", a) // float64
}
3.2、複合類型
合類型也叫做聚合類型。golang 中的複合類型只有兩種:數組和結構體
。它們是逐元素/欄位比較的。
註意:數組的長度視為類型的一部分,長度不同的兩個數組是不同的類型,不能直接比較。
- 對於數組來說,依次比較各個元素的值。根據元素類型的不同,再依據是基本類型、複合類型、引用類型或介面類型,按照特定類型的規則進行比較。所有元素全都相等,數組才是相等的。
- 對於結構體來說,依次比較各個欄位的值。根據欄位類型的不同,再依據是 4 中類型中的哪一種,按照特定類型的規則進行比較。所有欄位全都相等,結構體才是相等的。
註意:如包含了不支持直接使用 == 符號的類型,在編譯階段會報錯。
例如:
package main
import "fmt"
type Student struct {
Name string
Age int
Sex bool
}
type S1 struct {
Name string
Scores []int8 // 註意這裡定義的是 slice 類型
}
type ITest interface{}
func main() {
arrayA := [...]int64{2, 3, 4}
arrayB := [...]int64{2, 3, 4}
arrayC := [...]int64{1, 3, 4}
fmt.Println(arrayA == arrayB) // true
fmt.Println(arrayB == arrayC) // false
fmt.Println("-------")
s1 := Student{"xiaoming", 18, false}
s2 := Student{"xiaoming", 18, false}
s3 := Student{"xiaowang", 18, false}
fmt.Println(s1 == s2) // true
fmt.Println(s1 == s3) // false
fmt.Println("-------")
a1 := [...]Student{s1, s2}
// 註意這兩個元素!
a2 := [2]Student{s2, s2}
a3 := [2]Student{s2, s3}
fmt.Println(a1 == a2) // true
fmt.Println(a3 == a2) // false
fmt.Println("-------")
var i1 ITest = 23
var i2 ITest = 23
var i3 ITest = "tt"
var i4 ITest = 23
fmt.Println(i1 == i2) // true
fmt.Println(i3 == i4) // false
is1 := [...]ITest{i1, i2}
is2 := [...]ITest{i1, i4}
is3 := [...]ITest{i1, i3}
fmt.Println(is1 == is2) // true
fmt.Println(is1 == is3) // false
fmt.Println("-------")
t1 := S1{"xw", []int8{66, 88}}
t2 := S1{"xw", []int8{66, 88}}
t3 := S1{"xw", []int8{66, 99}}
// 為什麼這裡會報錯呢,因為我們定義的結構體中的 Score 欄位是 slice, slice 是不支持使用 == 符號的
// Invalid operation: t1 == t2 (the operator == is not defined on S1)
//fmt.Println(t1 == t2)
// Invalid operation: t1 == t2 (the operator == is not defined on S1)
//fmt.Println(t1 == t3)
// go 中 slice 使用 reflect.DeepEqual 判斷是否相等
fmt.Println(reflect.DeepEqual(t1, t2)) // true
fmt.Println(reflect.DeepEqual(t1, t3)) // false
}
3.3、引用類型
引用類型是指那些底層數據結構的值是引用地址(指針)的類型
。它們在記憶體中存儲的是指向實際數據的指針,而不是實際數據本身。切片、映射、通道和函數都是引用類型,因為它們在底層都使用了指針來引用實際的數據。
引用類型的比較實際判斷的是兩個變數是不是指向同一份數據,它不會去比較實際指向的數據。
關於引用類型,有幾個比較特殊的規定:
- 切片之間不允許比較。切片只能與
nil
值比較。 map
之間不允許比較。map
只能與nil
值比較。函數
之間不允許比較。函數
只能與nil
值比較。
接下來我們在仔細看看各個類型的具體介紹。
3.3.1、指針
package main
import (
"fmt"
)
type Student struct {
Name string
Age int
Sex bool
}
func main() {
s1 := &Student{"xiaoming", 18, false}
s2 := &Student{"xiaoming", 18, false}
s3 := s1
fmt.Println(s1 == s2) // false
fmt.Println(s1 == s3) // true
}
s1 和 s2 雖然數據一樣,但是他們在記憶體中的地址並不相等,所以他們是不相等的,s1 和 s3 指向的是同一份記憶體地址,所以是相等的。
3.3.2、channel 和 函數類型
接下來我們再看看 channel 和 函數類型:
package main
import "fmt"
type Student struct {
Name string
Age int
Sex bool
}
func main() {
ch1 := make(chan bool, 1)
ch2 := make(chan bool, 1)
ch3 := ch1
fmt.Println(ch1 == ch2) // false
fmt.Println(ch1 == ch3) // true
fmt.Println("-----")
a := TestFunc
b := TestFunc
c := a
// invalid operation: a == b (func can only be compared to nil)
//fmt.Println(a == b)
// invalid operation: a == c (func can only be compared to nil)
//fmt.Println(a == c)
fmt.Println(a) // 0x10a3400
fmt.Println(b) // 0x10a3400
fmt.Println(c) // 0x10a3400
}
func TestFunc() {
}
從上面可以看出來,函數類型不支持直接判等操作。原因是:函數類型不支持直接的判等操作是因為函數類型是一種複雜的類型,它包含了函數的簽名和實現代碼等信息。由於函數可以是閉包,可能捕獲了外部變數,因此函數的判等操作會涉及到比較函數的底層實現和捕獲的變數等細節,這會導致判等操作的複雜性和不確定性。
所以從中也可以看出來 Go 中判斷引用類型是否相等,不是簡單的判斷變數所在的記憶體地址是否一致,而是根據相應的類型,有不同的判斷規則,這裡大家需要註意。
3.3.3、slice
再看看切片。因為切片是引用類型,它可以間接的指向自己。例如:
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)
如果實在是需要判斷 slice 中元素是否相等,我們一般是自定義一個 判斷函數或者使用reflect.DeepEqual
函數。
package main
import (
"fmt"
"reflect"
)
func slicesAreEqual(slice1, slice2 []int) bool {
if len(slice1) != len(slice2) {
return false
}
for i := 0; i < len(slice1); i++ {
if slice1[i] != slice2[i] {
return false
}
}
return true
}
func main() {
slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3}
slice3 := []int{4, 5, 6}
fmt.Println(reflect.DeepEqual(slice1, slice2)) // 輸出: false (reflect.DeepEqual 可以進行值相等判斷)
fmt.Println(slicesAreEqual(slice1, slice2)) // 輸出: true
fmt.Println(slicesAreEqual(slice1, slice3)) // 輸出: false
}
註意,在上面的示例中,我們自定義了
slicesAreEqual
函數來判斷兩個切片是否擁有相同的元素。這個示例中我們使用了reflect.DeepEqual
來進行值相等的判斷,但是不推薦在切片的值相等判斷中使用reflect.DeepEqual
,因為它會將切片的元素逐個進行深度比較,效率較低,尤其在切片較大時。通常最好手動遍歷比較切片的元素。
3.3.4、map
在 Go 中,map
類型不支持直接的判等操作,因為 map
是一個引用類型,並不存儲在變數中的實際數據,而是一個指向底層數據結構的指針。map
是一種哈希表的實現,它包含了鍵值對的集合。
當你將一個 map
賦值給另一個變數時,它們引用同一個底層的 map
數據。因此,兩個 map
可能引用相同的底層數據,但它們仍然是不同的 map
對象。直接比較兩個 map
是否相等,並不能確定它們是否引用同一個底層數據。
如果你需要判斷兩個 map
是否包含相同的鍵值對,你可以通過手動遍歷比較 map
的鍵值對來實現。這涉及到比較每個鍵值對的鍵和值是否相等。
package main
import (
"fmt"
"reflect"
)
func mapsAreEqual(map1, map2 map[string]int) bool {
if len(map1) != len(map2) {
return false
}
for key, value := range map1 {
if map2Value, ok := map2[key]; !ok || map2Value != value {
return false
}
}
return true
}
func main() {
map1 := map[string]int{"a": 1, "b": 2, "c": 3}
map2 := map[string]int{"a": 1, "b": 2, "c": 3}
map3 := map[string]int{"a": 1, "b": 2, "c": 4}
// invalid operation: map1 == map2 (map can only be compared to nil)
//fmt.Println(map1 == map2)
fmt.Println(reflect.DeepEqual(map1, map2)) // 輸出: true (reflect.DeepEqual 可以進行值相等判斷)
fmt.Println(mapsAreEqual(map1, map2)) // 輸出: true
fmt.Println(mapsAreEqual(map1, map3)) // 輸出: false
}
在上面的示例中,我們自定義了mapsAreEqual
函數來判斷兩個 map
是否包含相同的鍵值對。請註意,與前面提到的reflect.DeepEqual
一樣,我們也不推薦在 map
的值相等判斷中使用reflect.DeepEqual
,因為它會將 map
的鍵值對逐個進行深度比較,效率較低,尤其在 map
較大時。通常最好手動遍歷比較 map
的鍵值對。
註意:
由於map
的底層原理是使用到了 hash 表,所以所有不可比較的類型都不能作為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
,編譯報錯。
3.4、介面類型
以下內容來自後面的參考鏈接 深入理解Go之== ,十分感謝原博文作者。
介面類型的值可以是任意一個實現了該介面的類型值,所以介面值除了需要記錄具體值之外,還需要記錄這個值屬於的類型。也就是說介面值由“類型”和“值”組成,鑒於這兩部分會根據存入值的不同而發生變化,我們稱之為介面的動態類型
和動態值
。
介面值的比較涉及這兩部分的比較,只有當動態類型完全相同且動態值相等(動態值使用==
比較),兩個介面值才是相等的。
package main
import "fmt"
func main() {
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
,故兩者不等。
package main
import "fmt"
func main() {
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
。
介面值的比較不要求介面類型(註意不是動態類型)完全相同,只要一個介面可以轉化為另一個就可以比較。例如:
package main
import (
"fmt"
"io"
"os"
)
func main() {
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)
}
type ReadCloser interface {
Reader
Closer
}
r
的類型為io.Reader
介面,rc
的類型為io.ReadCloser
介面。查看源碼,io.ReadCloser
的定義如下:
io.ReadCloser
可轉化為io.Reader
,故兩者可比較。
而io.Writer
不可轉化為io.Reader
,編譯報錯。
4、註意事項
不可比較性:
前面說過,golang 中的切片類型、map類型、函數類型(func)
是不可比較的。所有含有切片的類型都是不可比較的。例如:
- 數組元素是切片類型、map類型、函數類型(func)。
- 結構體有切片類型、map類型、函數類型(func)的欄位。
- 指針指向的是切片類型、map類型、函數類型(func)。
不可比較性會傳遞,如果一個結構體由於含有切片欄位不可比較,那麼將它作為元素的數組不可比較,將它作為欄位類型的結構體不可比較。
package main
import "fmt"
func main() {
type T struct {
a map[string]bool
}
t1 := T{
a: map[string]bool{"ni": true},
}
t2 := T{
a: map[string]bool{"ni": true},
}
// invalid operation: t1 == t2 (struct containing map[string]bool cannot be compared)
fmt.Println(t1 == t2)
type T1 struct {
a func()
}
t3 := T1{
a: func() {},
}
t4 := T1{
a: func() {},
}
// invalid operation: t1 == t2 (struct containing func() cannot be compared)
fmt.Println(t3 == t4)
}
關於引用類型,有幾個比較特殊的規定:
- 切片之間不允許比較。切片只能與
nil
值比較。 map
之間不允許比較。map
只能與nil
值比較。函數
之間不允許比較。函數
只能與nil
值比較。
參考鏈接: