在 Go 中如何讓結構體不可比較?
最近我在使用 Go 官方出品的結構化日誌包 slog
時,看到 slog.Value
源碼中有一個比較好玩的小 Tips,可以限制兩個結構體之間的相等性比較,本文就來跟大家分享下。
在 Go 中結構體可以比較嗎?
在 Go 中結構體可以比較嗎?這其實是我曾經面試過的一個問題,我們來做一個實驗:
定義如下結構體:
type Normal struct {
a string
B int
}
使用這個結構體分別聲明 3 個變量 n1
、n2
、n3
,然後進行比較:
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 個變量 n1
、n2
,然後進行比較:
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
類型字段,不可比較。
所以小結一下:
結構體是否可以比較,不取決於字段是否可導出,而是取決於其是否包含不可比較字段。
如果全部字段都是可比較的,那麼這個結構體就是可比較的。
如果其中有一個字段不可比較,那麼這個結構體就是不可比較的。
不過雖然我們不可以使用 ==
對 n1
、n2
進行比較,但我們可以使用 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
的兩個實例v1
、v2
的內存大小。你可以刪掉_ [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 中,歡迎點擊查看。
希望此文能對你有所啓發。
延伸閱讀
-
slog 源碼: https://github.com/golang/go/blob/master/src/log/slog/value.go#L21
-
Golang Tip #50: Make Structs Non-comparable.: https://x.com/func25/status/1768621711929311620
-
Go 語言編程技巧: https://colobu.com/gotips/050.html
-
本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/struct/non-comparable
聯繫我
-
公衆號:Go 編程世界
-
微信:jianghushinian
-
郵箱:jianghushinian007@outlook.com
-
博客:https://jianghushinian.cn
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/i1_NPJd6majyJTbn3-bsJg