Go 語言類型可比性
前段時間,一位讀者私信了我一個 Go 代碼例子,並問我這是不是一個 bug。我覺得蠻有意思的,故整理出了本文的分享內容。
在討論代碼之前,讀者需要有一些前置知識。
Go 可比較類型
在 Go 中,數據類型可以被分爲兩類,可比較與不可比較。兩者區分非常簡單:類型是否可以使用運算符 ==
和 !=
進行比較。
那哪些類型是可比較的呢?
-
Boolean(布爾值)、Integer(整型)、Floating-point(浮點數)、Complex(複數)、String(字符)這些類型是毫無疑問可以比較的。
-
Poniter (指針) 可以比較:如果兩個指針指向同一個變量,或者兩個指針類型相同且值都爲 nil,則它們相等。注意,指向不同的零大小變量的指針可能相等,也可能不相等。
-
Channel (通道)具有可比性:如果兩個通道值是由同一個 make 調用創建的,則它們相等。
c1 := make(chan int, 2)
c2 := make(chan int, 2)
c3 := c1
fmt.Println(c3 == c1) // true
fmt.Println(c2 == c1) // false
-
Interface (接口值)具有可比性:如果兩個接口值具有相同的動態類型和相等的動態值,則它們相等。
-
當類型 X 的值具有可比性且 X 實現 T 時,非接口類型 X 的值 x 和接口類型 T 的值 t 具有可比性。如果 t 的動態類型與 X 相同且 t 的動態值等於 x,則它們相等。
-
如果所有字段都具有可比性,則 struct (結構體值)具有可比性:如果它們對應的非空字段相等,則兩個結構體值相等。
-
如果 array(數組)元素類型的值是可比較的,則數組值是可比較的:如果它們對應的元素相等,則兩個數組值相等。
哪些類型是不可比較的?
- slice、map、function 這些是不可以比較的,但是也有特殊情況,那就是當他們值是 nil 時,可以與 nil 進行比較。
動態類型
在上文接口可比性中,我們提到了動態類型與動態值,這裏需要介紹一下。
常規變量(非接口)的類型是由聲明所定義,這是靜態類型,例如 var x int
。
接口類型的變量有一個獨特的動態類型,它是運行時存儲在變量中的值的實際類型。動態類型在執行過程中可能會有所不同,但始終可以分配給接口變量一個靜態類型。
例如
var someVariable interface{} = 101
someVariable
變量的靜態類型是 interface{}
,但是它的動態類型是 int
,並且很可能在之後發生變化。
var someVariable interface{} = 101
someVariable = 'Gopher'
如上, someVariable
變量的動態類型從 int
變爲了 string
。
代碼場景示例
我們爲當前業務所需的數據模型定義一個結構體 Data,它包含兩個字段:一個 string 類型的 UUID
和 interface{} 類型的 Content
。
type Data struct {
UUID string
Content interface{}
}
根據上文介紹, string 類型和 interface 是可比較類型,那麼兩個 Data 類型的數據,我們可以通過 ==
操作符進行比較。
var x, y Data
x = Data{
UUID: "856f5555806443e98b7ed04c5a9d6a9a",
Content: 1,
}
y = Data{
UUID: "745dee7719304991862e6985ea9c02a9",
Content: 2,
}
fmt.Println(x == y)
但是,如果在 Content 中的動態類型是 map 會怎樣?
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)
此時,我們程序編譯通過,但會發生運行時錯誤。
panic: runtime error: comparing uncomparable type map[int]string
那針對這種需求:即對於不可比較類型,因爲不能使用比較操作符 ==
,但我們想要比較它們包含的值是否相等時,應該怎麼辦。
此時我們可以採用 reflect.DeepEqual
函數進行比較,即將上述的 m==n
替換
fmt.Println(reflect.DeepEqual(m,n)) // true
我們得出結論:如果我們的變量中包含不可比較類型,或者 interface 類型(它的動態類型可能存在不可比較的情況),那麼我們直接運用比較運算符 ==
,會引發程序錯誤。此時應該選用 reflect.DeepEqual
函數(當然也有特殊情況,例如 []byte,可以通過 bytes. Equal 函數進行比較)。
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
對於同一個原始數據,經過 json 的 Marshal 和 Unmarshal 過程後,竟然不相等了?難道有 bug?
不慌,這種時候,我們直接上調試看看。
debug
原來此 1 非彼 1,Content 字段的數據類型由 int
轉換爲了 float64
。而在接口中,其動態類型不一致時,它的比較是不相等的。
經過排查,發現問題就出在 Unmarshal
函數上:如果要將 Json 對象 Unmarshal 爲接口值,那麼它的類型轉換規則如下
Unmarshal
可以看到,數值型的 json 解析操作統一爲了 float64。
因此,如果我們將 Content: 1
改爲 Content: 1.0
,那麼它 reflect.DeepEqual(x, y)
的值將是 true
。
增強型 DeepEqual 函數
針對 json 解析的這種類型改變特性,我們可以基於 reflect.DeepEqual
函數進行改造適配。
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)
}
當我們使用增強後的函數來運行上述的 “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
此時,結果符合我們的預期。
結論
本文討論了 Go 的可比較與不可比較類型,並對靜態、動態類型進行了闡述。
不可比較類型包括 slice、map、function,它們不能使用 ==
進行比較。雖然我們可以通過 ==
操作符對 interface 進行比較,由於動態類型的存在,如果實現 interface 的 T 有不可比較類型,這將引起運行時錯誤。
在不能確定 interface 的實現類型的情況下,對 interface 的比較,可以使用 reflect.DeepEqual
函數。
最後,我們通過 json 庫的解析與反解析過程中,發現了 json 解析存在數據類型轉換操作。這一個細節,讀者在使用過程中需要注意,以免產生想法 “這代碼有 bug” 。
參考
https://golang.org/ref/spec#Comparison_operators
https://golang.org/ref/spec#Types
https://pkg.go.dev/encoding/json#Unmarshal
機器鈴砍菜刀
歡迎添加小菜刀微信
加入 Golang 分享羣學習交流!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_a5kd6bBMEToCPZNR7D4Pw