天天看點

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

作者:不秃頭程式員
在 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 本身也在多處使用了這種用法:

在 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