Go 的设计是一种简单哲学,它摒弃了其他语言一些臃肿的功能和模块,以降低程序员的学习门槛,减少使用中的心智负担。

本文,我们来探讨 Go 中缺失的数据结构:Set,以及它的最佳实现方案。

Set 语义与实现方案

Set 集合是其他语言中常见的数据结构。特性:集合中的对象不按特定的方式排序,并且没有重复对象。

学习 Go ,要记住:Go 没有包含的东西,不代表 Go 真的没有。根据 Set 特性,我们可以很轻松地想到使用 map 的实现方案(因为 map 的 key 是不重复的):把对象当做 key 存入 map。

使用 map 来实现 Set,意味着我们只关心 key 的存在,其 value 值并不重要。有其他语言编程经验的人也许会选择 bool 来作为 value,因为它是其它语言中内存消耗最少的类型(1个字节)。但是在 Go 中,还有另一种选择:struct{}

fmt.Println(unsafe.Sizeof(struct {}{})) // output: 0

压测对比

为了探究哪种数据结构是作为 value 的最佳选择。我们选择了以下常用的类型作为 value 进行测试:boolintinterface{}struct{}

package mainimport ("testing"
)const num = int(1 << 24)// 测试 bool 类型
func Benchmark_SetWithBoolValueWrite(b *testing.B) {set := make(map[int]bool)for i := 0; i < num; i++ {set[i] = true}
}// 测试 interface{} 类型
func Benchmark_SetWithInterfaceValueWrite(b *testing.B) {set := make(map[int]interface{})for i := 0; i < num; i++ {set[i] = struct{}{}}
}// 测试 int 类型
func Benchmark_SetWithIntValueWrite(b *testing.B) {set := make(map[int]int)for i := 0; i < num; i++ {set[i] = 0}
}// 测试 struct{} 类型
func Benchmark_SetWithStructValueWrite(b *testing.B) {set := make(map[int]struct{})for i := 0; i < num; i++ {set[i] = struct{}{}}
}

我们运行以下命令,进行测试

$ go test -v -bench=. -count=3 -benchmem | tee result.txt
goos: darwin
goarch: amd64
pkg: workspace/example/demoForSet
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
Benchmark_SetWithBoolValueWrite
Benchmark_SetWithBoolValueWrite-8                1 3549312568 ns/op 883610264 B/op   614311 allocs/op
Benchmark_SetWithBoolValueWrite-8                1 3288521519 ns/op 883599440 B/op   614206 allocs/op
Benchmark_SetWithBoolValueWrite-8                1 3264097496 ns/op 883578624 B/op   614003 allocs/op
Benchmark_SetWithInterfaceValueWrite
Benchmark_SetWithInterfaceValueWrite-8           1 4397757645 ns/op 1981619632 B/op   614062 allocs/op
Benchmark_SetWithInterfaceValueWrite-8           1 4088301215 ns/op 1981553392 B/op   613743 allocs/op
Benchmark_SetWithInterfaceValueWrite-8           1 3990698218 ns/op 1981560880 B/op   613773 allocs/op
Benchmark_SetWithIntValueWrite
Benchmark_SetWithIntValueWrite-8                 1 3472910194 ns/op 1412326480 B/op   615131 allocs/op
Benchmark_SetWithIntValueWrite-8                 1 3519755137 ns/op 1412187928 B/op   614294 allocs/op
Benchmark_SetWithIntValueWrite-8                 1 3459182691 ns/op 1412057672 B/op   613390 allocs/op
Benchmark_SetWithStructValueWrite
Benchmark_SetWithStructValueWrite-8              1 3126746088 ns/op 802452368 B/op   614127 allocs/op
Benchmark_SetWithStructValueWrite-8              1 3161650835 ns/op 802431240 B/op   613632 allocs/op
Benchmark_SetWithStructValueWrite-8              1 3160410871 ns/op 802440552 B/op   613748 allocs/op
PASS
ok   workspace/example/demoForSet 42.660s

此时的结果看起来不太直观,这里推荐一个 benchmark 统计工具:Benchstat。通过以下命令进行安装

$ go get -u golang.org/x/perf/cmd/benchstat

使用 benchstat 分析刚才得到的 benchmark 结果文件

$ benchstat result.txt
name                           time/op
_SetWithBoolValueWrite-8        3.37s ± 5%
_SetWithInterfaceValueWrite-8   4.16s ± 6%
_SetWithIntValueWrite-8         3.48s ± 1%
_SetWithStructValueWrite-8      3.15s ± 1%name                           alloc/op
_SetWithBoolValueWrite-8        884MB ± 0%
_SetWithInterfaceValueWrite-8  1.98GB ± 0%
_SetWithIntValueWrite-8        1.41GB ± 0%
_SetWithStructValueWrite-8      802MB ± 0%name                           allocs/op
_SetWithBoolValueWrite-8         614k ± 0%
_SetWithInterfaceValueWrite-8    614k ± 0%
_SetWithIntValueWrite-8          614k ± 0%
_SetWithStructValueWrite-8       614k ± 0%

从内存开销而言,struct{} 是最小的,反映在执行时间上也是最少的。由于 bool 类型仅占一个字节,它相较于空结构而言,相差的并不多。但是,如果使用 interface{} 类型,那差距就很明显了。

所以,毫无疑问,在 Set 的实现中, map 值类型应该选 struct{}。

总结

本文虽然讨论的是 Set 的实现方案,但本质是涉及空结构体 struct{}{} 的 零内存特性。

空结构体除了是实现 Set 的 value 值最佳方案,它还可以应用于以下方面:

  • 通知信号的 channel:当 channel 只用于通知 goroutine 的执行事件,此时 channel 就不需要发送任何实质性的数据,选择使用 chan struct{}

  • 没有状态数据的结构体:当对象只拥有方法,而不包含任何的属性字段时,选择使用空结构体定义该对象。

关于作者

本篇内容转载自公众号「Golang 技术分享」,号主:机器铃砍菜刀,专注 Go 语言领域的技术分享,擅长源码分析,喜欢的可以关注。

数据结构--Go 语言中 Set 的最佳实现方案相关推荐

  1. c语言中 char怎样用,C语言中char*和char[]用法区别分析

    C语言中char*和char[]用法区别分析 本文实例分析了C语言中char* 和 char []的区别.分享给大家供大家参考之用.具体分析如下: 一般来说,很多人会觉得这两个定义效果一样,其实差别很 ...

  2. C语言中printf是不是关键字,C语言中printf是什么意思

    换行. printf("\n")表示输出换行符,"\n"是个转义字符,系统识别到转义字符时会自动换行.窗口是不会显示\n的,会直接换到下一行. 在不同的语言中, ...

  3. C++学习——c语言和C++语言中的struct

    C语言struct和C++struct区别 C语言中:struct是用户自定义数据类型(UDT): C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能 ...

  4. c语言中区别一般变量,C语言中,为了区别一般的变量,符号常量必须用

    C语言中,为了区别一般的变量,符号常量必须用大写字母表示.(?) 答:错 ,维也纳古典乐派代表人物之一,欧洲古典主义时期作曲家.因其对古典音乐的重大贡献,对奏鸣曲式和交响曲套曲结构的发展和创新,而被后 ...

  5. c语言 recv_sin,C++_C语言中经socket接收数据的相关函数详解,recv()函数: 头文件:#incl - phpStudy...

    C语言中经socket接收数据的相关函数详解 recv()函数:头文件: #include #include 定义函数: int recv(int s, void *buf, int len, uns ...

  6. c语言中extern关键字_了解C语言中的extern关键字

    c语言中extern关键字 In this article, we'll take a look at understanding the extern keyword in C. 在本文中,我们将了 ...

  7. 在c语言中 实参与其对应的形参各占独立的存储单元,以下正确的说法是( )。在C语言中: A.实参和与其对应的形参各占用独立的存储单元...

    以下正确的说法是( ).在C语言中: A.实参和与其对应的形参各占用独立的存储单元 关注:90  答案:6  mip版 解决时间 2021-01-27 22:41 提问者花开不败 2021-01-26 ...

  8. Go语言中的Map和List实现有序Map

    Go语言中的Map和List实现有序Map Map定义: Go 中 Map是一种无序的键值对的集合.Map最重要的一点是通过key来快速检索数据,key类似于索引,指向数据的值.Map是一种集合,所以 ...

  9. c语言中scanf(%d%*c, n);的意思

    c语言中scanf("%d%*c", &n);的意思. 2018年07月22日 17:08:00 韩小妹 阅读数:3274 scanf()中%*表示忽略掉一个输入项.上面的 ...

最新文章

  1. 禁用windows更新完成后的重启提示
  2. 写有效率的SQL查询(V)
  3. 如何安装gnuplot
  4. iOS中去除 Warning警告
  5. Spring3.2.4集成quartz2.2.1定时任务(demo).
  6. [转] 移动平台Html5的viewport使用经验
  7. RabbitMQ(1) - win+rabbitMQ
  8. ux.form.field.Year 只能选年的时间扩展
  9. java字节转xml_关于XML文档和JAVA中的JTree之间如何转换的问题
  10. 京东成全国首批支持第三方商家接入数字人民币的企业
  11. unity4.6 failed to update unity web player
  12. SAP工具箱 多表导入程序
  13. 视频教程-2020年软考系统分析师--案例分析真题精解视频课程-软考
  14. ARP表 MAC表 路由表
  15. Unity塔防游戏学习(六)
  16. 加拿大比索大学计算机科学硕士,来悉尼大学恍恍惚惚一年后,我给大家吐血整理了经验贴…....
  17. u3d 100道面试题(包含答案)
  18. java ee jpi是什么,“JPI”是“Java Plug In”的缩写,意思是“Java插件”
  19. win11如何开启电脑高性能模式?
  20. 渗透测试成功的8个关键

热门文章

  1. 【Keras】减少过拟合的秘诀——Dropout正则化
  2. kbmmw 与extjs 通过JSON Base64 显示图片
  3. 线程轮循打印ABC...
  4. 以数据为中心的存储观
  5. 【转】C#获取当前日期时间(转)
  6. 2.请求安全-- MD5的必要性以及实际应用场景
  7. Python: try finally 与 上下文管理器简介
  8. QTP中VBS脚本下FSO、WSH的应用(二)
  9. 萧功秦:为什么我们缺少特立独行的人生态度
  10. springboot整合视图层之freemarker