在 Go 中如何讓結(jié)構(gòu)體不可比較?
共 5533字,需瀏覽 12分鐘
·
2024-06-19 08:52
最近我在使用 Go 官方出品的結(jié)構(gòu)化日志包 slog 時,看到 slog.Value 源碼中有一個比較好玩的小 Tips,可以限制兩個結(jié)構(gòu)體之間的相等性比較,本文就來跟大家分享下。
在 Go 中結(jié)構(gòu)體可以比較嗎?
在 Go 中結(jié)構(gòu)體可以比較嗎?這其實是我曾經(jīng)面試過的一個問題,我們來做一個實驗:
定義如下結(jié)構(gòu)體:
type Normal struct {
a string
B int
}
使用這個結(jié)構(gòu)體分別聲明 3 個變量 n1、n2、n3,然后進(jìn)行比較:
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)
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
true
false
可見 Normal 結(jié)構(gòu)體是可以比較的。
如何讓結(jié)構(gòu)體不可比較?
那么所有結(jié)構(gòu)體都可以比較嗎?顯然不是,如果都可以比較,那么 reflect.DeepEqual() 就沒有存在的必要了。
定義如下結(jié)構(gòu)體:
type NoCompare struct {
a string
B map[string]int
}
使用這個結(jié)構(gòu)體分別聲明 2 個變量 n1、n2,然后進(jìn)行比較:
n1 := NoCompare{
a: "a",
B: map[string]int{
"a": 10,
},
}
n2 := NoCompare{
a: "a",
B: map[string]int{
"a": 10,
},
}
fmt.Println(n1 == n2)
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
./main.go:59:15: invalid operation: n1 == n2 (struct containing map[string]int cannot be compared)
這里程序直接報錯了,并提示結(jié)構(gòu)體包含了 map[string]int 類型字段,不可比較。
所以小結(jié)一下:
結(jié)構(gòu)體是否可以比較,不取決于字段是否可導(dǎo)出,而是取決于其是否包含不可比較字段。
如果全部字段都是可比較的,那么這個結(jié)構(gòu)體就是可比較的。
如果其中有一個字段不可比較,那么這個結(jié)構(gòu)體就是不可比較的。
不過雖然我們不可以使用 == 對 n1、n2 進(jìn)行比較,但我們可以使用 reflect.DeepEqual() 對二者進(jìn)行比較:
fmt.Println(reflect.DeepEqual(n1, n2))
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
true
更優(yōu)雅的做法
最近我在使用 Go 官方出品的結(jié)構(gòu)化日志包 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
}
可以發(fā)現(xiàn),這里有一個匿名字段 _ [0]func(),并且注釋寫著 // disallow ==。
_ [0]func() 的目的顯然是為了禁止比較。
我們來實驗一下,_ [0]func() 是否能夠?qū)崿F(xiàn)禁止結(jié)構(gòu)體相等性比較:
v1 := Value{
num: 1,
any: 2,
}
v2 := Value{
num: 1,
any: 2,
}
fmt.Println(v1 == v2)
執(zhí)行示例代碼,輸出結(jié)果如下:
$ go run main.go
./main.go:109:15: invalid operation: v1 == v2 (struct containing [0]func() cannot be compared)
可以發(fā)現(xiàn),的確有效。因為 func() 是一個函數(shù),而函數(shù)在 Go 中是不可比較的。
既然使用 map[string]int 和 _ [0]func() 都能實現(xiàn)禁止結(jié)構(gòu)體相等性比較,那么我為什么說 _ [0]func() 是更優(yōu)雅的做法呢?
_ [0]func() 有著比其他實現(xiàn)方式更優(yōu)的特點:
它不占內(nèi)存空間!
使用匿名字段 _ 語義也更強。
而且,我們直接去 Go 源碼里搜索,能夠發(fā)現(xiàn)其實 Go 本身也在多處使用了這種用法:
所以推薦使用 _ [0]func() 來實現(xiàn)禁用結(jié)構(gòu)體相等性比較。
不過值得注意的是:當(dāng)使用 _ [0]func() 時,不要把它放在結(jié)構(gòu)體最后一個字段,推薦放在第一個字段。這與結(jié)構(gòu)體內(nèi)存對齊有關(guān),我在《Go 中空結(jié)構(gòu)體慣用法,我?guī)湍憧偨Y(jié)全了!》 一文中也有提及。
NOTE: 對于
_ [0]func()不占用內(nèi)存空間的驗證,就交給你自己去實驗了。提示:可以使用fmt.Println(unsafe.Sizeof(v1), unsafe.Sizeof(v2))分別打印結(jié)構(gòu)體Value的兩個實例v1、v2的內(nèi)存大小。你可以刪掉_ [0]func()字段再試一試。
總結(jié)
好了,在 Go 中如何讓結(jié)構(gòu)體不可比較這個小 Tips 就分享給大家了,還是比較有意思的。
我在看到 slog.Value 源碼使用 _ [0]func() 來禁用結(jié)構(gòu)體相等性比較時,又搜索了 Go 的源碼中多處在使用,我想這應(yīng)該是社區(qū)推薦的做法了。然后就嘗試去網(wǎng)上搜索了下,還真被我搜索到了一個叫 Phuong Le 的人在 X 上發(fā)布了 Golang Tip #50: Make Structs Non-comparable. 專門來介紹這個 Tip,并且我在中文社區(qū)也找到了鳥窩老師在《Go語言編程技巧》中的譯文 Tip #50 使結(jié)構(gòu)體不可比較。
這也印證了我的猜測,_ [0]func() 在 Go 社區(qū)中是推薦用法。
本文示例源碼我都放在了 GitHub 中,歡迎點擊查看。
希望此文能對你有所啟發(fā)。
延伸閱讀
-
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
推薦閱讀
