在 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() 时,不要把它放在结构体最后一个字段,推荐放在第一个字段 。这与结构体内存对齐 有关,我在《Go 中空结构体惯用法,我帮你总结全了!》 一文中也有提及。

NOTE: 对于 _ [0]func() 不占用内存空间的验证,就交给你自己去实验了。
提示:可以使用 fmt.Println(unsafe.Sizeof(v1), unsafe.Sizeof(v2)) 分别打印结构体 Value 的两个实例 v1v2 的内存大小。你可以删掉 _ [0]func() 字段再试一试。

总结

好了,在 Go 中如何让结构体不可比较这个小 Tips 就分享给大家了,还是比较有意思的。

前端的世界总是在不断变化,作为开发者,我们需要保持好奇心和学习热情,不断探索新的技术,只有这样,我们才能在这个快速发展的时代中立于不败之地。低代码也是一个值得我们深入探索的领域,让我们拭目以待,它将给前端世界带来怎样的变革。

介绍一款程序员都应该知道的软件JNPF快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。

JNPF可以实现应用从创建、配置、开发、测试到发布、运维、升级等完整生命周期的管理。减少了传统应用程序的代码编写量,通过图形化、可视化的界面,以拖放组件的方式,即可快速生成应用程序的产品,大幅降低了开发企业管理类软件的难度。

希望这篇文章对你有所帮助~

相关推荐
ZJ_.12 分钟前
Node.js 使用 gRPC:从定义到实现
java·开发语言·javascript·分布式·rpc·架构·node.js
techlead_krischang24 分钟前
中国软件评测中心最新报告:文心大模型技术、产品、应用全面领跑
后端·go
yjjpp230127 分钟前
Django REST Framework(四)DRF APIVIEW
后端·python·django
concisedistinct31 分钟前
大数据开发语言 Scala(四):面向对象编程
大数据·开发语言·后端·scala·编程语言·面向对象
_Rookie._38 分钟前
java 单例模式
java·开发语言·单例模式
梦中北山1 小时前
JWT安全分析
开发语言·网络·安全·web安全·php
纸月1 小时前
短信服务(二):实现某短信服务商崩溃后自动切换并重试
后端·go
编程刘明1 小时前
学会拥抱Python六剑客,提高编程效率
开发语言·数据库·python·程序人生·职场和发展·学习方法