背景

Golang语言本身未实现set,但是实现了map

golang的map是一种无序的键值对的集合,其中键是唯一的

而set是键的不重复的集合,因此可以用map来实现set

Empty

由于map是key-value集合,如果使用map来实现set,则不需要关注value的具体类型和值

struct{}是具有零个元素的struct,struct{}的大小为0,不占用空间,因此十分适合作为value使用

type Empty struct{}

Int64HashSet

Golang是静态强类型语言,对于int8、uint8、int64、uint64、 string基础数据类型的set,均需要实现类似的代码

定义

type Int8HashSet map[int8]Empty
type UintHashSet map[uint8]Empty
type Int64HashSet map[int64]Empty
type Uint64HashSet map[uint64]Empty
type Int64HashSet map[string]Empty  

以int64为例,实现set的基本操作

初始化

func NewInt64HashSet(cap ...int) Int64HashSet {
var set Int64HashSet
if len(cap) == 0 {
set = make(Int64HashSet)
} else {
set = make(Int64HashSet, cap[0])
}
return set
}

插入

func (set Int64HashSet) Insert(items ...int64) {
for _, item := range items {
set[item] = Empty{}
}
}

删除

func (set Int64HashSet) Delete(items ...int64) {
for _, item := range items {
delete(set, item)
}
}

列表

func (set Int64HashSet) List() []int64 {
list := make([]int64, 0, len(set))
for item := range set {
list = append(list, item)
}
return list
}

弊端

采用上面的方法实现,会充斥着大量重复代码,对于其它类型如int8,uint8,string等类型,需要单独实现,尽管逻辑基本一致。

在Go 1.18版本之前,我们可以使用反射来避免这个问题,

使用反射在运行时推断具体的类型,虽然有性能上的损耗,但是单次纳秒级别的操作,基本可以忽略不计。

HashSet

interface{}是没有方法的空接口,所有类型都实现了空接口

通过反射可以从interface获取对象的值和类型

定义

type HashSet map[interface{}]Empty

初始化

func NewHashSet(cap ...int) HashSet {
var set HashSet
if len(cap) == 0 {
set = make(HashSet)
} else {
set = make(HashSet, cap[0])
}
return set
}

插入

func (set HashSet) Insert(items ...interface{}) {
for _, item := range items {
set[item] = Empty{}
}
}

删除

func (set HashSet) Delete(items ...interface{}) {
for _, item := range items {
delete(set, item)
}
}

列表

// 通过反射获取到具体的类型
// 可以将int64替换为其它类型,如uint8, string等
func (set HashSet) ListInt64() []int64 {
list := make([]int64, 0, len(set))
for item := range set {
if val, ok := item.(int64); ok {
list = append(list, val)
}
}
return list
} func (set HashSet) ListString() []string {
list := make([]string, 0, len(set))
for item := range set {
if val, ok := item.(string); ok {
list = append(list, val)
}
}
return list
}

GenericHashSet

反射在编译时缺少类型检查,比如对于同一个set,先后插入int类型和string类型数据,在编译和运行阶段均不会报错。

hash := NewHashSet(8)
// 插入int类型
hash.Insert(111)
// 插入string类型
hash.Insert("string")

使用反射在一定程度上避免了大量的重复代码,但是将set转换为slice还是会存在重复的相似逻辑的代码

并且需要在运行时获取/判断对象的类型和值,存在一定的性能损耗

在Go 1.18版本提供了范型(Generics)的支持,

范型可以在编译期间进行类型检查和类型推断,相对于反射机制而言,性能有所提升

定义

type GenericHashSet[T comparable] map[T]Empty

初始化

func NewGenericHashSet[T comparable](cap ...int) *GenericHashSet[T] {
var set GenericHashSet[T]
if len(cap) == 0 {
set = make(GenericHashSet[T])
} else {
set = make(GenericHashSet[T], cap[0])
}
return &set
}

插入

func (set *GenericHashSet[T]) Insert(items ...T) {
for _, item := range items {
(*set)[item] = Empty{}
}
}

删除

func (set *GenericHashSet[T]) Delete(items ...T) {
for _, item := range items {
delete(*set, item)
}
}

列表

func (set *GenericHashSet[T]) List() []T {
list := make([]T, 0, len(*set))
for item := range *set {
list = append(list, item)
}
return list
}

性能对比

插入操作测试代码

func BenchmarkInt64HashSetInsert(b *testing.B) {
intHashSet := NewInt64HashSet()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
intHashSet.Insert(rand.Int63())
}
} func BenchmarkGenericHashSetInsert(b *testing.B) {
gHashSet := NewGenericHashSet[int64]()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
gHashSet.Insert(rand.Int63())
}
} func BenchmarkHashSetInsert(b *testing.B) {
hashSet := NewHashSet()
rand.Seed(time.Now().UnixNano())
for i := 0; i < b.N; i++ {
hashSet.Insert(rand.Int63())
}
}

插入操作测试结果

zbwdeAir:set zbw$ go test -bench="BenchmarkInt64HashSetInsert|BenchmarkGenericHashSetInsert|BenchmarkHashSetInsert" -benchmem
goos: darwin
goarch: arm64
pkg: set/set
BenchmarkInt64HashSetInsert-8 10051916 119.2 ns/op 40 B/op 0 allocs/op
BenchmarkGenericHashSetInsert-8 13957741 123.7 ns/op 57 B/op 0 allocs/op
BenchmarkHashSetInsert-8 6526810 188.9 ns/op 63 B/op 1 allocs/op
PASS
ok set/set 4.897s

可以看出来,Int64HashSet性能最优,GenericHashSet次之,HashSet性能最差。

从实际使用角度看

对于Go < 1.18版本,使用HashSet即可。如果追求性能的极致,不介意大量重复代码,那还是使用Int64HashSet

对于单次操作的时间在ns级别,对于大部分业务场景,反射带来的性能损耗基本可以忽略,性能的瓶颈并不在这里。

对于Go >= 1.18版本,可以使用GenericHashSet

其它

如果需要实现有序set,则需要链表辅助实现

详细代码,见github

如果你觉得还可以,点一下Star

Golang实现set的更多相关文章

  1. Golang, 以17个简短代码片段,切底弄懂 channel 基础

    (原创出处为本博客:http://www.cnblogs.com/linguanh/) 前序: 因为打算自己搞个基于Golang的IM服务器,所以复习了下之前一直没怎么使用的协程.管道等高并发编程知识 ...

  2. 说说Golang的使用心得

    13年上半年接触了Golang,对Golang十分喜爱.现在是2015年,离春节还有几天,从开始学习到现在的一年半时间里,前前后后也用Golang写了些代码,其中包括业余时间的,也有产品项目中的.一直 ...

  3. TODO:Golang指针使用注意事项

    TODO:Golang指针使用注意事项 先来看简单的例子1: 输出: 1 1 例子2: 输出: 1 3 例子1是使用值传递,Add方法不会做任何改变:例子2是使用指针传递,会改变地址,从而改变地址. ...

  4. Golang 编写的图片压缩程序,质量、尺寸压缩,批量、单张压缩

    目录: 前序 效果图 简介 全部代码 前序: 接触 golang 不久,一直是边学边做,边总结,深深感到这门语言的魅力,等下要跟大家分享是最近项目 服务端 用到的图片压缩程序,我单独分离了出来,做成了 ...

  5. golang struct扩展函数参数命名警告

    今天在使用VSCode编写golang代码时,定义一个struct,扩展几个方法,如下: package storage import ( "fmt" "github.c ...

  6. golang语言构造函数

    1.构造函数定义 构造函数 ,是一种特殊的方法.主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中.特别的一个类可以有多个构造函数 ,可根据其参数个 ...

  7. TODO:Golang语言TCP/UDP协议重用地址端口

    TODO:Golang语言TCP/UDP协议重用地址端口 这是一个简单的包来解决重用地址的问题. go net包(据我所知)不允许设置套接字选项. 这在尝试进行TCP NAT时尤其成问题,其需要在同一 ...

  8. golang的安装

    整理了一下,网上关于golang的安装有三种方式(注明一下,我的环境为CentOS-6.x, 64bit) 方式一:yum安装(最简单) rpm -Uvh http://dl.fedoraprojec ...

  9. golang枚举类型 - iota用法拾遗

    在c#.java等高级语言中,经常会用到枚举类型来表示状态等.在golang中并没有枚举类型,如何实现枚举呢?首先从枚举的概念入手. 1.枚举类型定义 从百度百科查询解释如下:http://baike ...

  10. golang 使用 iota

    iota是golang语言的常量计数器,只能在常量的表达式中使用. iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(io ...

随机推荐

  1. Kotlin学习快速入门(7)——扩展的妙用

    原文地址: Kotlin学习快速入门(7)--扩展的妙用 - Stars-One的杂货小窝 之前也模模糊糊地在用这个功能,也是十分方便,可以不用继承,快速给某个类增加新的方法,本篇便是来讲解下Kotl ...

  2. Ubuntu14.04.6配置阿里源

    Ubuntu14.04.6配置阿里源 这两天上手 Ubuntu 系统,因为公司用的是 14.04.6 版本,所以有了一些踩坑记录. 起因是安装完系统我需要安装一个搜狗输入法,过程得安装 fcitx,需 ...

  3. 开启apache2的ssl访问功能

    Ubuntu 20.04 1. Apache2默认安装的时候,ssl模块是不启用的.开启命令: $ sudo apt install apache2 #安装$ sudo a2enmod ssl #开启 ...

  4. 线程池的概念&原理和线程池的代码实现

    线程池:一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源.工作原理:可以用一张图来简洁明了说明: 合理利用线程池能够带来三个好处∶1.降低 ...

  5. Spring 核心概念

    Spring 核心概念 引言 本文主要介绍 Spring 源码中使用到的一些核心类 1. BeanDefinition BeanDefinition表示Bean定义,BeanDefinition 中存 ...

  6. 树莓派实战:微信机器人(itchat实现)

    背景 楼主有一台树莓派4B开发板(8G内存版),是目前的顶配机型.这一年来的业余时间,除了写Java.架构方面的文章,也陆续折腾了不少树莓派上的好玩小项目,在此新开一个树莓派实战的文章系列,分享给粉丝 ...

  7. 安卓fastboot刷机、刷magisk、aidlux备忘

    环境就不多说了,网上一堆教程,我只在这边简单记录一下,以小米手机为例 刷机 解锁bootloader PC上配置好adb.fastboot,也就是platform-tools工具包加入系统变量,在命令 ...

  8. LNMP架构及DISCUZ论坛部署

    1)(5分)服务器IP地址规划:client:12.0.0.12/24,网关服务器:ens36:12.0.0.1/24.ens33:172.16.10.1/24,Web1:172.16.10.10/2 ...

  9. 基于ABP实现DDD--领域逻辑和应用逻辑

      本文主要介绍了多应用层的问题,包括原因和实现.通过理解介绍了如何区分领域逻辑和应用逻辑,哪些是正确的实践,哪些是不推荐的或者错误的实践. 一.多应用层的问题 1.多应用层介绍   不知道你们是否会 ...

  10. 2022了你还不会『低代码』?数据科学也能玩转Low-Code啦! ⛵

    作者:韩信子@ShowMeAI 数据分析实战系列:http://www.showmeai.tech/tutorials/40 机器学习实战系列:http://www.showmeai.tech/tut ...