熟悉 Java 的看到这种标题,下意识会觉得这篇文章可能是分析 Go 中 set 的实现细节,但我要告诉你的是,Go 官方并没有 set 这种东西,需要的话自己去实现吧… 于是有了这篇文章。知其然且知其所以然,Go 官方之所以还不提供原生的 set, 究其原因,个人猜测与 Go1 无泛型有关,map 和 slice 是 Go 在编译器层面做了手脚以支持『泛型』。可以阅读 Go blog 的如下声明:

If we can write generic types, we can define new data structures, like these, that have the same type-checking advantages as slices and maps: the compiler can statically type-check the types of the values that they hold, and the values can be stored as themselves, not as interface types.

有了其他语言实现 set 的先例,我们依葫芦画瓢也是可以造出来的,以 Java 为例,我们可以基于 Go 的 map 来实现 set, 由于仅需要 key, 所以 value 部分我们能省则省。最开始我想到的是使用 bool, 毕竟空间够小嘛,但是,经过一番搜索后,我们可以发现有一种叫『empty struct』的东西,它占用的空间是 0!

如果只是使用 set, 目前的 golang-set 流传较广,可以试试,虽然线程安全的 set 实现是直接使用读写锁这种粗粒度的锁。

Go collections

Go set 的轮子其实已经不少了,八仙过海各显神通,为什么我还要重新发明轮子呢?一是为 Go 生态添砖加瓦;二是想在 set 中夹带私货,一些自己需要的 feature 可以自己按需实现;三是 set 实现也确实不难,练练手也是极好的。

综上,我就 fork 了一个 go-collections, 逐步完善 set, queue 等一系列基础数据结构。目前 set 的实现,加了 Foreach, MapUnmarshalText(toml 解析用) 这些有意思的实现,其他移植了部分 Java 和 Python 中常用的一些方法。

并发 Set - ConcurrentSet

为了尽可能减少锁的使用,并发 Set 的实现内部的 Map 借助了官方的 sync.Map, 这个 Map 采用空间换时间的思路优化读多写少的场景,但遗憾的是考虑到 Map size 并不是典型应用场景及性能的 tradeoff, 官方并未加入 Len 方法,综合考虑,我决定使用 atomic 来记录增删过程中的 Map 长度。这也是 ConcurrentSet 的实现背景。

Reference