一道比較運(yùn)算符相關(guān)的面試題把我虐的體無完膚
雜(貨鋪)談
今天這篇文章相對(duì)來說比較基礎(chǔ),大家花幾分鐘時(shí)間看看,有所收獲自然是最好,沒有收獲也就消磨幾分鐘時(shí)間罷了,你不虧,筆者也不虧~
前幾期還是有一定難度的HTTP系列文章,今天卻是畫風(fēng)突變講起了基礎(chǔ),這當(dāng)然是因?yàn)榛A(chǔ)重要呀。正所謂萬丈高樓平地起,我們夯實(shí)基礎(chǔ),樓才能建的高,畢竟精美的小矮樓總是很容易被高樓遮擋。嗨,扯遠(yuǎn)了,總之筆者今天寫這篇文章絕對(duì)不是下面這個(gè)原因:
累呀!真的累!工作嘛?當(dāng)然不是!前幾期分析HTTP系列文章確實(shí)耗費(fèi)了太多精力,周末連續(xù)熬夜就算是鐵打的人也扛不住,所以本周只有養(yǎng)精蓄銳這一個(gè)目的。
雜(貨鋪)言
Go中的比較操作符,你真的了解嗎?假如面試官問你下面輸出什么,你的答案是什么?
type blankSt struct {
a int
_ string
}
var (
bst1 = blankSt{1, "333"}
bst2 = blankSt{1, "44444"}
)
fmt.Println(bst1 == bst2)
今天,筆者總結(jié)了一份比較運(yùn)算符的相關(guān)文檔,助力讀者夯實(shí)基礎(chǔ)(答案請(qǐng)參考后文)。
基本定理
在Go中,比較運(yùn)算符也是遵循定理的, 兩條基本定理如下:
定理一:相等運(yùn)算符==和!=適用于具有可比性的操作數(shù),排序運(yùn)算符<,<=,>和>=適用于可排序的操作數(shù)。
定理二:在任何比較中,至少滿足一個(gè)操作數(shù)能賦值給另一個(gè)操作數(shù)類型的變量。
常見類型的比較
常見類型的比較大家都懂,在這里筆者就不詳細(xì)介紹了,僅枚舉一下原則加深大家的印象:
布爾值是可比較的,但不可排序,即僅適用于
==和!=運(yùn)算符。整數(shù)和浮點(diǎn)數(shù)是可比較的且可排序,適用于所有比較運(yùn)算符。
字符串的值是可比較的,且按字節(jié)排序,即比較時(shí)按字節(jié)比較大小(不理解字符串和字節(jié)切片關(guān)系的,請(qǐng)參考深入理解go中字符串這篇文章)。
以上即為常見類型的比較原則,下面我們結(jié)合例子逐步理解各種類型之間的比較。
不可比較的類型
在Go中,切片,map,和func是不可比較的,他們僅可以和預(yù)聲明表示符nil進(jìn)行比較。能和nil進(jìn)行比較的還有指針,管道和interface{}。
復(fù)數(shù)之間的比較
復(fù)數(shù)可比較但不可排序,實(shí)部和虛部均相等兩個(gè)復(fù)數(shù)才相等。復(fù)數(shù)僅適用==和!=這兩個(gè)比較運(yùn)算符:
var c1 complex128 = complex(1, 2) // 1+2i
var c2 complex128 = complex(3, 4) // 3+4i
fmt.Println(c1 == c2)
上述輸出結(jié)果為false,證明復(fù)數(shù)之間可比較。
var c1 complex128 = complex(1, 2) // 1+2i
var c2 complex128 = complex(3, 4) // 3+4i
fmt.Println(c1 >= c2)
此時(shí)程序無法運(yùn)行,在vscode中的錯(cuò)誤提醒為cannot compare c1 >= c2 (operator >= not defined for complex128)compiler。由此確認(rèn)復(fù)數(shù)不可排序。
結(jié)構(gòu)體之間的比較
如果結(jié)構(gòu)體的所有字段都是可比較的,則他們所有非匿名字段的值相等,結(jié)構(gòu)體的值才相等。驗(yàn)證如下:
type canC struct {
c int
}
var st1, st2 canC
fmt.Println(st1 == st2)
st1.c = 3
fmt.Println(st1 == st2)
上述輸出分別為true和false。由此驗(yàn)證非匿名字段的值相等,結(jié)構(gòu)體的值才相等。
fmt.Println(st1 <= st2)
此行代碼在vscode中的錯(cuò)誤提醒為cannot compare st1 <= st2 (operator <= not defined for canC)compiler。由此可知,即使結(jié)構(gòu)體滿足比較條件也無法適用于<,<=,>和>=運(yùn)算符。
注:后文中提到不可排序均代表著無法適用于<,<=,>和>=運(yùn)算符,在后文中不再對(duì)不可排序給出例子。
下面看看包含匿名字段的結(jié)構(gòu)體比較時(shí)有什么不同:
type blankSt struct {
a int
_ string
}
var (
bst1 = blankSt{1, "333"}
bst2 = blankSt{1, "44444"}
)
fmt.Println(bst1 == bst2)
上述輸出為true,符合非匿名字段的值相等時(shí)結(jié)構(gòu)體的值相等這一原則。注意,如果匿名字段是不可比較的類型時(shí),上述代碼會(huì)編譯報(bào)錯(cuò)。
最后我們看看包含不可比較類型的結(jié)構(gòu)體:
type canNotC struct {
m func() int
}
fmt.Println(canNotC{} == canNotC{})
上述代碼在vscode中的錯(cuò)誤提醒為cannot compare (canNotC literal) == (canNotC literal) (operator == not defined for canNotC)compiler,由此可知,結(jié)構(gòu)體如果要可比較,則其內(nèi)部的所有字段必須全為可比較類型。
指針之間的比較
指針是可比較的,但不可排序。如果兩個(gè)指針指向同一個(gè)變量,或者兩個(gè)指針值均為nil,則這兩個(gè)指針相等。相信讀者對(duì)于這一點(diǎn)應(yīng)該是沒有異議的,但是有一個(gè)情況卻是十分需要注意的。
zero-size variables:如果結(jié)構(gòu)體沒有任何字段或者數(shù)組沒有任何元素,則其大小為0,即unsafe.Sizeof的計(jì)算結(jié)果為0。兩個(gè)不同的zero-size variables在內(nèi)存中可能具有相同的地址。
指向不同zero-size variables的兩個(gè)指針可能相等也可能不相等。
var arr1, arr2 [0]int
parr1 := &arr1
parr2 := &arr2
fmt.Println(unsafe.Sizeof(arr1))
fmt.Println(parr1 == parr2)
fmt.Println(uintptr(unsafe.Pointer(parr1)), uintptr(unsafe.Pointer(parr2)))
// 輸出如下:
0
false
824634830552 824634830552 // 每次運(yùn)行輸出的地址不一定相同
筆者多次運(yùn)行,parr1 == parr2始終輸出為false,目前尚未發(fā)現(xiàn)輸出為true的情況,在https://github.com/golang/go/issues/23440也有人遇到同筆者相同的情況,所以筆者就不再對(duì)此問題做更近一步的分析。
Channel之間的比較
在寫這篇文章前,筆者從來都沒有想過Channel之間是可以比較的。事實(shí)上,管道是可比較類型,golang原文如下:
Channel values are comparable.
Two channel values are equal if they were created by the same call to make or if both have value nil
這里需要注意的是,只有相同調(diào)用的管道才是相等的:
var cint1, cint2 chan<- string
cint3 := make(chan string, 2)
cint4 := make(chan string, 2)
cint5 := make(chan string)
fmt.Println(cint1 == cint2, cint3 == cint4, cint5 == cint1) // true false false
cint1 = cint4
fmt.Println(cint1 == cint4) //true
上述中,cint1和cint2初始值均為nil,所以輸出true。雙向通道cint4賦值給單向通道cint1時(shí),滿足相同的make調(diào)用這一條件,所以輸出也為true。
Interface{}之間比較
Interface{}是可比較的,但是不可排序。兩個(gè)Interface{}變量的動(dòng)態(tài)類型和動(dòng)態(tài)value均一致它們才相等,兩個(gè)變量均為nil也是相等的。針對(duì)這一原則筆者對(duì)其分以下幾種情況討論。
一、interface{}不為nil,且動(dòng)態(tài)類型均為可比較類型時(shí):
var (
i1 interface{} = uint(1)
i2 interface{} = uint(1)
i3 interface{} = uint(3)
i4 interface{} = int(3)
i5 interface{} = []int{}
i6 interface{} = map[int]string{}
i7 interface{} = map[int]string{}
)
fmt.Println(i1 == i2, i1 == i3,i3 == i4)
上述輸出結(jié)果為true false false,這符合動(dòng)態(tài)類型和動(dòng)態(tài)value均一致時(shí)才相等的原則。
二、如果比較雙方動(dòng)態(tài)類型一致且為不可比較類型時(shí)會(huì)panic:
這種情況可正常編譯,但是會(huì)造成運(yùn)行時(shí)崩潰,所以一定要注意!!!
fmt.Println(i5 == i6)
fmt.Println(i7 == i6)
上述比較i5和i6時(shí)能夠正常輸出,但是i6和i7比較時(shí)出現(xiàn)如下錯(cuò)誤:

所以,筆者在這里再次強(qiáng)調(diào),如果項(xiàng)目中有不小心直接使用了interface{}進(jìn)行比較的,請(qǐng)一定要注意??。
三、動(dòng)態(tài)value為nil的interface{}不一定等于nil:
func t() interface{} {
var err *error
return err
}
func t1() interface{} {
return nil
}
fmt.Println(t() == nil, t1() == nil) // 輸出false, true
由上述代碼知,如果不是直接返回nil的interface{}和nil進(jìn)行比較時(shí)是不相等的。相信很多人在平時(shí)的開發(fā)中都有可能會(huì)忽略這個(gè)問題。下面我們對(duì)它為什么不相等進(jìn)行簡(jiǎn)單的分析。
在Go中,interface{}的實(shí)現(xiàn)為兩個(gè)元素,類型t和值v。v是一個(gè)具體的值,如int、struct、或指針等。
如果,我們?cè)诮涌谥写鎯?chǔ)int值3,則接口中的值為(t=int,v=3)。
值v也稱為接口的動(dòng)態(tài)值,因?yàn)樵诔绦驁?zhí)行期間,給定的接口變量可能包含不同的值v(以及相應(yīng)的類型t)。
只有當(dāng)v和t都未設(shè)置時(shí),接口值才是nil(t=nil,未設(shè)置v)。
如果,我們?cè)诮涌谥写鎯?chǔ)一個(gè)類型為int的nil指針, 那么不管指針的值是什么,內(nèi)部類型都會(huì)是int:(t=*int,v=nil)。因此,即使內(nèi)部的指針v為nil,這樣的接口值也是非nil的。
本部分內(nèi)容翻譯整理自https://golang.org/doc/faq#nil_error
非接口類型X實(shí)現(xiàn)了接口T,則X的變量x能和T的變量t進(jìn)行比較
原則:非接口類型X實(shí)現(xiàn)了接口T,則X的變量x能和T的變量t進(jìn)行比較。只有當(dāng)t的動(dòng)態(tài)變量類型為X且t的動(dòng)態(tài)value和x相等時(shí),t才等于x。
推論:又因?yàn)間o中任意類型都默認(rèn)實(shí)現(xiàn)了interface{},則意味著interface{}變量能和任意的非interface{}類型的可比較類型進(jìn)行比較。
驗(yàn)證原則:
type it interface {
f()
}
type ix1 int
func (x ix1) f() {}
type ix2 map[int]int
func (x ix2) f() {}
x1 := ix1(2)
var t1 it = ix2{}
// 類型不一致時(shí)
fmt.Println(x1 == t1) // fasle
// 類型一致時(shí)
t1 = ix1(2)
fmt.Println(t1 == x1) // true
上面的輸出分別為false和true,符合原則。
驗(yàn)證推論:
var it1 interface{} = "111"
fmt.Println(it1 == 1)
上面能夠正常比較,說明推論正確,且輸出為false,符合原則。
注意:下面情況會(huì)panic
var t2 it = ix2{}
var t3 it = ix2{}
fmt.Println(t2 == t3)
上述代碼發(fā)生panic,符和Interface{}之間比較的原則。
數(shù)組之間的比較
兩個(gè)數(shù)組的元素類型相同且是可比較類型,并且數(shù)組的長度相同,則這兩個(gè)數(shù)組可比較。當(dāng)兩個(gè)可比較的數(shù)組對(duì)應(yīng)元素均相等時(shí),則這兩個(gè)數(shù)組相等。即使兩個(gè)數(shù)組可比較,但依舊不可排序。
類型相同但元素不可比較時(shí):
var array1 [3][]int
var array2 [3][]int
fmt.Println(array1 == array2)
上述代碼在vscode中的錯(cuò)誤為cannot compare array1 == array2 (operator == not defined for [3][]int)compiler,所以如果數(shù)組元素為不可比較類型,則數(shù)組也不可比較。
數(shù)組元素可比較但數(shù)組長度不一致時(shí):
var array3 [3]int
var array4 [2]int
fmt.Println(array3 == array4)
上述代碼在vscode中的錯(cuò)誤為cannot compare array3 == array4 (mismatched types [3]int and [2]int)compiler,所以如果數(shù)組長度不一致時(shí),則兩個(gè)數(shù)組不可比較。
滿足數(shù)組長度相等且元素類型可比較時(shí):
var array5, array6 [3]int
fmt.Println(array5 == array6)
array5 = [...]int{3, 2, 1}
array6 = [...]int{1, 2, 3}
fmt.Println(array5 == array6)
上述輸出分別為true和false,符合可比較數(shù)組一一判斷對(duì)應(yīng)元素是否相等這一原則。所以,我們平時(shí)在開發(fā)中可以利用該原則快速比較數(shù)組是否相等。
最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。
注:
寫本文時(shí), 筆者所用go版本為: go1.14.2
文章中所用完整例子:https://github.com/Isites/go-coder/blob/master/comparison-operators/main.go
參考:
https://golang.org/ref/spec#Comparison_operators
推薦閱讀

