在 Go 中如何让结构体不可比较?

最近我在使用 Go 官方出品的结构化日志包 slog 时,看到 slog.Value 源码中有一个比较好玩的小 Tips,可以限制两个结构体之间的相等性比较,本文就来跟大家分享下。

在 Go 中结构体可以比较吗?

在 Go 中结构体可以比较吗?这其实是我曾经面试过的一个问题,我们来做一个实验:

定义如下结构体:

go 复制代码
type Normal struct {
	a string
	B int
}

使用这个结构体分别声明 3 个变量 n1n2n3,然后进行比较:

go 复制代码
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)

执行示例代码,输出结果如下:

bash 复制代码
$ go run main.go
true
false

可见 Normal 结构体是可以比较的。

如何让结构体不可比较?

那么所有结构体都可以比较吗?显然不是,如果都可以比较,那么 reflect.DeepEqual() 就没有存在的必要了。

定义如下结构体:

go 复制代码
type NoCompare struct {
	a string
	B map[string]int
}

使用这个结构体分别声明 2 个变量 n1n2,然后进行比较:

go 复制代码
n1 := NoCompare{
	a: "a",
	B: map[string]int{
		"a": 10,
	},
}
n2 := NoCompare{
	a: "a",
	B: map[string]int{
		"a": 10,
	},
}

fmt.Println(n1 == n2)

执行示例代码,输出结果如下:

bash 复制代码
$ 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() 对二者进行比较:

go 复制代码
fmt.Println(reflect.DeepEqual(n1, n2))

执行示例代码,输出结果如下:

bash 复制代码
$ go run main.go
true

更优雅的做法

最近我在使用 Go 官方出品的结构化日志包 slog 时,看到 slog.Value 源码:

go 复制代码
// 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() 是否能够实现禁止结构体相等性比较:

go 复制代码
v1 := Value{
	num: 1,
	any: 2,
}
v2 := Value{
	num: 1,
	any: 2,
}

fmt.Println(v1 == v2)

执行示例代码,输出结果如下:

bash 复制代码
$ 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() 时,不要把它放在结构体最后一个字段,推荐放在第一个字段 。这与结构体内存对齐 有关,我在《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 中,欢迎点击查看。

希望此文能对你有所启发。

延伸阅读

联系我

相关推荐
木叶丸10 分钟前
编程开发中,那些你必须掌握的基本概念
前端·数据结构·编程语言
拖孩17 分钟前
微信群太多,管理麻烦?那试试接入AI助手吧~
前端·后端·微信
Humbunklung26 分钟前
Rust枚举:让数据类型告别单调乏味
开发语言·后端·rust
radient33 分钟前
Golang-GMP 万字洗髓经
后端·架构
Code季风34 分钟前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin
蓝倾34 分钟前
如何使用API接口实现淘宝商品上下架监控?
前端·后端·api
舂春儿36 分钟前
如何快速统计项目代码行数
前端·后端
Pedantic37 分钟前
我们什么时候应该使用协议继承?——Swift 协议继承的应用与思
前端·后端
Codebee38 分钟前
如何利用OneCode注解驱动,快速训练一个私有的AI代码助手
前端·后端·面试
martinzh38 分钟前
用Spring AI搭建本地RAG系统:让AI成为你的私人文档助手
后端