golang-set包的用法及源码解析

1、 背景介绍

Set是一种基本的数据结构,它具备确定性、互异性、无序性三个特点。因此,在开发过程中我们通常用它来判断一些数据的集合与另一个数据集合或者元素的包含关系。在大部分开发语言中set都是一种基本的数据结构,但是golang不提供set类型。通常情况下,我们都会用map[interface{}]struct{}{}来代替set实现包含关系的判断。但事实上,我们在github上会发现一些第三方的开源包。例如golang-set就是一个相对成熟的包。截止到2021年3月份已经有1.8k的star和160+的fork。同时这个包本身也已经应用于docker项目中。是一个可用性和可靠性都经过验证的第三方包,大家可以放心使用。

golang-set包本身也是基于map[interface{}]struct{}{}结构实现的,同时golang-set包提供了线程安全和不保证安全的两种set类型,相比于线程安全的set对象,不保证安全的set对象执行效率会更高一点。

2、使用方法

golang-set的使用也非常简单,只需导入该包然后创建set对象即可开始使用。

import mapset "github.com/deckarep/golang-set"

set := mapset.NewSet(1, 2, 3, 4)
set.Add(6)

set.Contains(5)
set.Remove(1)

golang 提供了两种set类型,一种是普通的集合对象,一种是线程安全的集合对象,通过结构嵌套的方式为普通集合对象新增了一个全局锁,实现了线程安全。具体如下:

type threadSafeSet struct {
    s threadUnsafeSet
    sync.RWMutex
}

func newThreadSafeSet() threadSafeSet {
    return threadSafeSet{s: newThreadUnsafeSet()}
}

golang-set包提供的集合操作方法包含如下23个方法:

type Set interface {
    Add(i interface{}) bool
    Cardinality() int
    Clear()
    Clone() Set
    Contains(i ...interface{}) bool
    Difference(other Set) Set
    Equal(other Set) bool
    Intersect(other Set) Set
    IsProperSubset(other Set) bool
    IsProperSuperset(other Set) bool
    IsSubset(other Set) bool
    IsSuperset(other Set) bool
    Each(func(interface{}) bool)
    Iter() <-chan interface{}
    Iterator() *Iterator
    Remove(i interface{})
    String() string
    SymmetricDifference(other Set) Set
    Union(other Set) Set
    Pop() interface{}
    PowerSet() Set
    CartesianProduct(other Set) Set
    ToSlice() []interface{}
}

具体含义如下:

  • Add(i interface{}) bool:用于向集合中添加某些元素

  • Cardinality() int :返回集合元素个数

  • Clear() :清空集合中全部元素,剩下一个空的集合,使用不需要通过NewSet进行初始化

  • Clone() Set :复制一个一模一样的集合对象

  • Contains(i ...interface{}) bool :返回是否参数元素全部包含于集合中

  • Difference(other Set) Set :将返回当前集合与参数集合的全部差异元素,这些元素包含于本集合但是不包含于参数集合。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • Equal(other Set) bool :如果参数集合与当前集合的容量和全部元素是相同的,那么会被然认为是相同的,无需考虑元素的顺序。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • Intersect(other Set) Set:将返回两个集合的交集。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • IsProperSubset(other Set) bool:判断当前set是否为参数set的真子集(包含但不相等)。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • IsProperSuperset(other Set) bool:判断参数set是否为当前set的真子集(包含但不相等)。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • IsSubset(other Set) bool:判断当前set是否为参数set的子集(包含,允许相等)。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • IsSuperset(other Set) bool:判断参数set是否为当前set的子集(包含,允许相等)。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • Each(func(interface{}) bool):遍历元素,并对每个元素执行传递的func。如果传递的func返回true,则此时停止迭代。

    //clone a
    b := NewSet()
    a.Each(func(elem interface{}) bool {
        b.Add(elem)
        return false
    })
    
  • Iter() <-chan interface{}:返回一个可以遍历set的channel

    b := NewSet()
    for val := range a.Iter() {
        b.Add(val)
    }
    
  • Iterator() *Iterator:返回一个Iterator对象,用于遍历set的全部参数。

    b := NewThreadUnsafeSet()
    for val := range a.Iterator().C {
        b.Add(val)
    }
    
  • Remove(i interface{}) :从当前集合中移除某个元素

  • String() string:返回集合的string格式,set:{}用于查看该集合。

  • SymmetricDifference(other Set) Set:返回当前集合与参数集合的对称差集(对称差集中的元素要么包含于本集合,要么包含于参数集合,但是不能属于两者的交集)。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • Union(other Set) Set:返回当前集合与参数集合的并集。注意:入参的set类型与方法接受者set的类型必须一致,否则将导致panic。

  • Pop() interface{}:移除并返回一个随机的元素

  • PowerSet() Set:返回当前集合的全部子集

  • CartesianProduct(other Set) Set:返回当前集合与参数集合的笛卡尔积结果集合

  • ToSlice() []interface{}:返回当前集合的切片对象。

3、源码解析

由上文可知,golang-set提供了普通和线程安全两种类型的集合对象。其中线程安全的集合对象,通过结构嵌套的方式为普通集合对象新增了一个全局锁,实现了线程安全。因此针对方法的实现主要通过普通集合对象的实现来介绍。

golang-set包的具体实现其实非常简单,基于golang-set原生的map结构作为基本的存储结构,value以一个不占用内存空间的struct{}{}为值。其中一个我认为值得我们借鉴的开发技巧就是golang-set的遍历方法,具体源码如下所示:

func (set *threadUnsafeSet) Iter() <-chan interface{} {
    ch := make(chan interface{})
    go func() {
        for elem := range *set {
            ch <- elem
        }
        close(ch)
    }()

    return ch
}

它通过创建一个协程去遍历set对象,同时返回一个channel 由用户去读取set的遍历结果,是的读取较大set对象时,用户无需等待,而直接读取,具有较高的效率,但是他的缺点也十分明显,那就是这个方式无法中途退出,也就是说,在使用该方法遍历时,用户必须保证会执行完全部的遍历结果。否则,由于返回的channel是无缓冲的channel,用户不读取时,Iterator()方法中的遍历协程将阻塞,而无法退出,就会发生内存泄漏,具体案例参考goleng-set错误使用导致的内存泄漏

为了方便遍历方法的中途退出,golang-set又提供了另外一个遍历方法,通过返回一个美枚举对象来实现set对象的异步遍历。具体源码如下:

type Iterator struct {
    C    <-chan interface{}
    stop chan struct{}
}

func newIterator() (*Iterator, chan<- interface{}, <-chan struct{}) {
    itemChan := make(chan interface{})
    stopChan := make(chan struct{})
    return &Iterator{
        C:    itemChan,
        stop: stopChan,
    }, itemChan, stopChan
}


func (set *threadUnsafeSet) Iterator() *Iterator {
    iterator, ch, stopCh := newIterator()

    go func() {
    L:
        for elem := range *set {
            select {
            case <-stopCh:
                break L
            case ch <- elem:
            }
        }
        close(ch)
    }()

    return iterator
}

其中枚举对象Iterator中的C channel用于异步读取遍历结果,stop channel用于主动停止遍历协程,从而避免内存泄漏。具体的使用方法如下:

it := a.Iterator()

for v := range it.C {
    //退出条件
    if (v == 10){
        it.stop()
    }
}

以上就是本篇文章的全部内容。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容