Go 語言類型可比性

前段時間,一位讀者私信了我一個 Go 代碼例子,並問我這是不是一個 bug。我覺得蠻有意思的,故整理出了本文的分享內容。

在討論代碼之前,讀者需要有一些前置知識。

Go 可比較類型

在 Go 中,數據類型可以被分爲兩類,可比較與不可比較。兩者區分非常簡單:類型是否可以使用運算符  ==!= 進行比較。

那哪些類型是可比較的呢?

 c1 := make(chan int, 2)
 c2 := make(chan int, 2)
 c3 := c1

 fmt.Println(c3 == c1) // true
 fmt.Println(c2 == c1) // false

哪些類型是不可比較的?

動態類型

在上文接口可比性中,我們提到了動態類型與動態值,這裏需要介紹一下。

常規變量(非接口)的類型是由聲明所定義,這是靜態類型,例如 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