一篇學(xué)會(huì)Go 語(yǔ)言類型可比性
前段時(shí)間,一位讀者私信了我一個(gè) Go 代碼例子,并問我這是不是一個(gè) bug。我覺得蠻有意思的,故整理出了本文的分享內(nèi)容。
在討論代碼之前,讀者需要有一些前置知識(shí)。
Go 可比較類型
在 Go 中,數(shù)據(jù)類型可以被分為兩類,可比較與不可比較。兩者區(qū)分非常簡(jiǎn)單:類型是否可以使用運(yùn)算符 == 和 != 進(jìn)行比較。
那哪些類型是可比較的呢?
- Boolean(布爾值)、Integer(整型)、Floating-point(浮點(diǎn)數(shù))、Complex(復(fù)數(shù))、String(字符)這些類型是毫無(wú)疑問可以比較的。
- Poniter (指針) 可以比較:如果兩個(gè)指針指向同一個(gè)變量,或者兩個(gè)指針類型相同且值都為 nil,則它們相等。注意,指向不同的零大小變量的指針可能相等,也可能不相等。
- Channel (通道)具有可比性:如果兩個(gè)通道值是由同一個(gè) make 調(diào)用創(chuàng)建的,則它們相等。
- c1 := make(chan int, 2)
- c2 := make(chan int, 2)
- c3 := c1
- fmt.Println(c3 == c1) // true
- fmt.Println(c2 == c1) // false
- Interface (接口值)具有可比性:如果兩個(gè)接口值具有相同的動(dòng)態(tài)類型和相等的動(dòng)態(tài)值,則它們相等。
- 當(dāng)類型 X 的值具有可比性且 X 實(shí)現(xiàn) T 時(shí),非接口類型 X 的值 x 和接口類型 T 的值 t 具有可比性。如果 t 的動(dòng)態(tài)類型與 X 相同且 t 的動(dòng)態(tài)值等于 x,則它們相等。
- 如果所有字段都具有可比性,則 struct (結(jié)構(gòu)體值)具有可比性:如果它們對(duì)應(yīng)的非空字段相等,則兩個(gè)結(jié)構(gòu)體值相等。
- 如果 array(數(shù)組)元素類型的值是可比較的,則數(shù)組值是可比較的:如果它們對(duì)應(yīng)的元素相等,則兩個(gè)數(shù)組值相等。
哪些類型是不可比較的?
- slice、map、function 這些是不可以比較的,但是也有特殊情況,那就是當(dāng)他們值是 nil 時(shí),可以與 nil 進(jìn)行比較。
動(dòng)態(tài)類型
在上文接口可比性中,我們提到了動(dòng)態(tài)類型與動(dòng)態(tài)值,這里需要介紹一下。
常規(guī)變量(非接口)的類型是由聲明所定義,這是靜態(tài)類型,例如 var x int。
接口類型的變量有一個(gè)獨(dú)特的動(dòng)態(tài)類型,它是運(yùn)行時(shí)存儲(chǔ)在變量中的值的實(shí)際類型。動(dòng)態(tài)類型在執(zhí)行過程中可能會(huì)有所不同,但始終可以分配給接口變量一個(gè)靜態(tài)類型。
例如
- var someVariable interface{} = 101
someVariable 變量的靜態(tài)類型是 interface{},但是它的動(dòng)態(tài)類型是 int,并且很可能在之后發(fā)生變化。
- var someVariable interface{} = 101
- someVariable = 'Gopher'
如上, someVariable 變量的動(dòng)態(tài)類型從 int 變?yōu)榱?string。
代碼場(chǎng)景示例
我們?yōu)楫?dāng)前業(yè)務(wù)所需的數(shù)據(jù)模型定義一個(gè)結(jié)構(gòu)體 Data,它包含兩個(gè)字段:一個(gè) string 類型的 UUID 和 interface{} 類型的 Content。
- type Data struct {
- UUID string
- Content interface{}
- }
根據(jù)上文介紹, string 類型和 interface 是可比較類型,那么兩個(gè) Data 類型的數(shù)據(jù),我們可以通過 == 操作符進(jìn)行比較。
- var x, y Data
- x = Data{
- UUID: "856f5555806443e98b7ed04c5a9d6a9a",
- Content: 1,
- }
- y = Data{
- UUID: "745dee7719304991862e6985ea9c02a9",
- Content: 2,
- }
- fmt.Println(x == y)
但是,如果在 Content 中的動(dòng)態(tài)類型是 map 會(huì)怎樣?
- var m, n Data
- m = Data{
- UUID: "9584dba3fe26418d86252d71a5d78049",
- Content: map[int]string{1: "GO", 2: "Python"},
- }
- n = Data{
- UUID: "9584dba3fe26418d86252d71a5d78049",
- Content: map[int]string{1: "GO", 2: "Python"},
- }
- fmt.Println(m == n)
此時(shí),我們程序編譯通過,但會(huì)發(fā)生運(yùn)行時(shí)錯(cuò)誤。
- panic: runtime error: comparing uncomparable type map[int]string
那針對(duì)這種需求:即對(duì)于不可比較類型,因?yàn)椴荒苁褂帽容^操作符 ==,但我們想要比較它們包含的值是否相等時(shí),應(yīng)該怎么辦。
此時(shí)我們可以采用 reflect.DeepEqual 函數(shù)進(jìn)行比較,即將上述的 m==n 替換
- fmt.Println(reflect.DeepEqual(m,n)) // true
我們得出結(jié)論:如果我們的變量中包含不可比較類型,或者 interface 類型(它的動(dòng)態(tài)類型可能存在不可比較的情況),那么我們直接運(yùn)用比較運(yùn)算符 == ,會(huì)引發(fā)程序錯(cuò)誤。此時(shí)應(yīng)該選用 reflect.DeepEqual 函數(shù)(當(dāng)然也有特殊情況,例如 []byte,可以通過 bytes. Equal 函數(shù)進(jìn)行比較)。
Bug 代碼?
好,鋪墊了這么久,終于可以展示讀者給我的代碼了。
- var x, y Data
- x = Data{
- UUID: "856f5555806443e98b7ed04c5a9d6a9a",
- Content: 1,
- }
- bytes, _ := json.Marshal(x)
- _ = json.Unmarshal(bytes, &y)
- fmt.Println(x) // {856f5555806443e98b7ed04c5a9d6a9a 1}
- fmt.Println(y) // {856f5555806443e98b7ed04c5a9d6a9a 1}
- fmt.Println(reflect.DeepEqual(x, y)) // false
對(duì)于同一個(gè)原始數(shù)據(jù),經(jīng)過 json 的 Marshal 和 Unmarshal 過程后,竟然不相等了?難道有 bug?
不慌,這種時(shí)候,我們直接上調(diào)試看看。
debug
原來(lái)此 1 非彼 1,Content 字段的數(shù)據(jù)類型由 int 轉(zhuǎn)換為了 float64 。而在接口中,其動(dòng)態(tài)類型不一致時(shí),它的比較是不相等的。
經(jīng)過排查,發(fā)現(xiàn)問題就出在 Unmarshal 函數(shù)上:如果要將 Json 對(duì)象 Unmarshal 為接口值,那么它的類型轉(zhuǎn)換規(guī)則如下
Unmarshal
可以看到,數(shù)值型的 json 解析操作統(tǒng)一為了 float64。
因此,如果我們將 Content: 1 改為 Content: 1.0 ,那么它 reflect.DeepEqual(x, y) 的值將是 true。
增強(qiáng)型 DeepEqual 函數(shù)
針對(duì) json 解析的這種類型改變特性,我們可以基于 reflect.DeepEqual 函數(shù)進(jìn)行改造適配。
- func DeepEqual(v1, v2 interface{}) bool {
- if reflect.DeepEqual(v1, v2) {
- return true
- }
- bytesA, _ := json.Marshal(v1)
- bytesB, _ := json.Marshal(v2)
- return bytes.Equal(bytesA, bytesB)
- }
當(dāng)我們使用增強(qiáng)后的函數(shù)來(lái)運(yùn)行上述的 “bug” 例子
- var x, y Data
- x = Data{
- UUID: "856f5555806443e98b7ed04c5a9d6a9a",
- Content: 1,
- }
- b, _ := json.Marshal(x)
- _ = json.Unmarshal(b, &y)
- fmt.Println(DeepEqual(x, y)) // true
此時(shí),結(jié)果符合我們的預(yù)期。
結(jié)論
本文討論了 Go 的可比較與不可比較類型,并對(duì)靜態(tài)、動(dòng)態(tài)類型進(jìn)行了闡述。
不可比較類型包括 slice、map、function,它們不能使用 == 進(jìn)行比較。雖然我們可以通過 == 操作符對(duì) interface 進(jìn)行比較,由于動(dòng)態(tài)類型的存在,如果實(shí)現(xiàn) interface 的 T 有不可比較類型,這將引起運(yùn)行時(shí)錯(cuò)誤。
在不能確定 interface 的實(shí)現(xiàn)類型的情況下,對(duì) interface 的比較,可以使用 reflect.DeepEqual 函數(shù)。
最后,我們通過 json 庫(kù)的解析與反解析過程中,發(fā)現(xiàn)了 json 解析存在數(shù)據(jù)類型轉(zhuǎn)換操作。這一個(gè)細(xì)節(jié),讀者在使用過程中需要注意,以免產(chǎn)生想法“這代碼有 bug” 。
參考
https://golang.org/ref/spec#Comparison_operators
https://golang.org/ref/spec#Types
https://pkg.go.dev/encoding/json#Unmarshal