在 Go 中如何讓結構體不可比較?

最近我在使用 Go 官方出品的結構化日誌包 slog 時,看到 slog.Value 源碼中有一個比較好玩的小 Tips,可以限制兩個結構體之間的相等性比較,本文就來跟大家分享下。

在 Go 中結構體可以比較嗎?

在 Go 中結構體可以比較嗎?這其實是我曾經面試過的一個問題,我們來做一個實驗:

定義如下結構體:

type Normal struct {
 a string
 B int
}

使用這個結構體分別聲明 3 個變量 n1n2n3,然後進行比較:

n1 := Normal{
 a: "a",
 B: 10,
}
n2 := Normal{
 a: "a",
 B: 10,
}
n3 := Normal{
 a: "b",
 B: 20,
}

fmt.Println(n1 == n2)
fmt.Println(n1 == n3)

執行示例代碼,輸出結果如下:

$ go run main.go
true
false

可見 Normal 結構體是可以比較的。

如何讓結構體不可比較?

那麼所有結構體都可以比較嗎?顯然不是,如果都可以比較,那麼 reflect.DeepEqual() 就沒有存在的必要了。

定義如下結構體:

type NoCompare struct {
 a string
 B map[string]int
}

使用這個結構體分別聲明 2 個變量 n1n2,然後進行比較:

n1 := NoCompare{
 a: "a",
 B: map[string]int{
  "a": 10,
 },
}
n2 := NoCompare{
 a: "a",
 B: map[string]int{
  "a": 10,
 },
}

fmt.Println(n1 == n2)

執行示例代碼,輸出結果如下:

$ go run main.go
./main.go:59:15: invalid operation: n1 == n2 (struct containing map[string]int cannot be compared)

這裏程序直接報錯了,並提示結構體包含了 map[string]int 類型字段,不可比較。

所以小結一下:

結構體是否可以比較,不取決於字段是否可導出,而是取決於其是否包含不可比較字段。

如果全部字段都是可比較的,那麼這個結構體就是可比較的。

如果其中有一個字段不可比較,那麼這個結構體就是不可比較的。

不過雖然我們不可以使用 ==n1n2 進行比較,但我們可以使用 reflect.DeepEqual() 對二者進行比較:

fmt.Println(reflect.DeepEqual(n1, n2))

執行示例代碼,輸出結果如下:

$ go run main.go
true

更優雅的做法

最近我在使用 Go 官方出品的結構化日誌包 slog 時,看到 slog.Value 源碼:

// A Value can represent any Go value, but unlike type any,
// it can represent most small values without an allocation.
// The zero Value corresponds to nil.
type Value struct {
 _ [0]func() // disallow ==
 // num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration,
 // the string length for KindString, and nanoseconds since the epoch for KindTime.
 num uint64
 // If any is of type Kind, then the value is in num as described above.
 // If any is of type *time.Location, then the Kind is Time and time.Time value
 // can be constructed from the Unix nanos in num and the location (monotonic time
 // is not preserved).
 // If any is of type stringptr, then the Kind is String and the string value
 // consists of the length in num and the pointer in any.
 // Otherwise, the Kind is Any and any is the value.
 // (This implies that Attrs cannot store values of type Kind, *time.Location
 // or stringptr.)
 any any
}

可以發現,這裏有一個匿名字段 _ [0]func(),並且註釋寫着 // disallow ==

_ [0]func() 的目的顯然是爲了禁止比較。

我們來實驗一下,_ [0]func() 是否能夠實現禁止結構體相等性比較:

v1 := Value{
 num: 1,
 any: 2,
}
v2 := Value{
 num: 1,
 any: 2,
}

fmt.Println(v1 == v2)

執行示例代碼,輸出結果如下:

$ go run main.go
./main.go:109:15: invalid operation: v1 == v2 (struct containing [0]func() cannot be compared)

可以發現,的確有效。因爲 func() 是一個函數,而函數在 Go 中是不可比較的。

既然使用 map[string]int_ [0]func() 都能實現禁止結構體相等性比較,那麼我爲什麼說 _ [0]func() 是更優雅的做法呢?

_ [0]func() 有着比其他實現方式更優的特點:

它不佔內存空間!

使用匿名字段 _ 語義也更強。

而且,我們直接去 Go 源碼裏搜索,能夠發現其實 Go 本身也在多處使用了這種用法:

_ [0]func()

所以推薦使用 _ [0]func() 來實現禁用結構體相等性比較。

不過值得注意的是:當使用 _ [0]func() 時,不要把它放在結構體最後一個字段,推薦放在第一個字段。這與結構體內存對齊有關,我在《Go 中空結構體慣用法,我幫你總結全了!》 一文中也有提及。

NOTE: 對於 _ [0]func() 不佔用內存空間的驗證,就交給你自己去實驗了。提示:可以使用 fmt.Println(unsafe.Sizeof(v1), unsafe.Sizeof(v2)) 分別打印結構體 Value 的兩個實例 v1v2 的內存大小。你可以刪掉 _ [0]func() 字段再試一試。

總結

好了,在 Go 中如何讓結構體不可比較這個小 Tips 就分享給大家了,還是比較有意思的。

我在看到 slog.Value 源碼使用 _ [0]func() 來禁用結構體相等性比較時,又搜索了 Go 的源碼中多處在使用,我想這應該是社區推薦的做法了。然後就嘗試去網上搜索了下,還真被我搜索到了一個叫 Phuong Le 的人在 X 上發佈了 Golang Tip #50: Make Structs Non-comparable. 專門來介紹這個 Tip,並且我在中文社區也找到了鳥窩老師在《Go 語言編程技巧》中的譯文 Tip #50 使結構體不可比較。

這也印證了我的猜測,_ [0]func() 在 Go 社區中是推薦用法。

本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。

希望此文能對你有所啓發。

延伸閱讀

聯繫我

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/i1_NPJd6majyJTbn3-bsJg