在 Harvester 源码中读懂 golang-set:Go 泛型 Set 集合的接口设计、双实现与工程实践
云原生虚拟化存储后端高可用【免费下载链接】harvesterOpen source hyperconverged infrastructure (HCI) software项目地址https://gitcode.com/gh_mirrors/ha/harvester点击查看免费下载Go 标准库长期没有提供内置的 set 类型而去重、成员判断、集合运算在基础设施代码里是高频需求。本文以 Harvester 仓库 vendored 的第三方包github.com/deckarep/golang-set/v2下文简称 mapset的 README 为主线系统讲解这个Go 缺失的泛型 set 集合它如何用一个泛型接口同时提供线程安全与非线程安全两套实现、Set[T comparable]接口包含哪些操作、构造函数的取舍以及它的底层源码实现同时结合 Harvester 自身的 webhook 校验器与网络控制器的真实调用代码展示 mapset 在 Kubernetes 生态项目中的典型用法。读完后你既能独立使用这个库也能看懂它被锁在 vendor/github.com/deckarep/golang-set/v2/ 中的源码细节。1. 包定位与 Harvester 中的依赖版本mapset 的 README 开宗明义The missinggenericset collection for the Go language. Until Go has sets built-in...use this.在 Go 拥有内置 set 之前用它替代。它的设计动机是模仿 Python 的 set 集合membership testing、union、intersection、difference 等并补齐 Go 1.18 之后泛型带来的类型安全。README 中的版本信息如下打包版本2.2.0起重构为最小化指针间接引用的实现改善了方法文档规范并新增了Append批量追加和从已有Map构造新 set 等便捷方法支持新的泛型语法要求Go 1.18.0 或更高README 当时在 Go 1.20 上做了工作流测试。在 Harvester 仓库中这个包已经通过 Go module 的 vendor 机制完整落地依赖声明见 go.mod 中的github.com/deckarep/golang-set/v2 v2.6.0仓库工具链为go 1.26.0实际源码位于 vendor/github.com/deckarep/golang-set/v2/包含set.go包文档、Set[T comparable]接口与全部构造函数threadunsafe.go非线程安全实现map[T]struct{}threadsafe.go线程安全实现sync.RWMutex包装iterator.go基于 channel 的可提前终止迭代器sorted.goSorted排序导出函数//go:build go1.21README.md本文依据的原始文档。README 给出的安装方式为go get github.com/deckarep/golang-set/v2需要说明适用前提Harvester 仓库本身已经 vendored 该依赖版本锁定为 v2.6.0在仓库内开发时直接 import 即可无需执行go get该命令适用于你自己在其他项目中引入此依赖的场景。README 中被 Ethereum、Docker、1Password、HashiCorp 等项目使用等表述属于第三方包的自我宣传本文不作事实性转述。2. 泛型与 comparable哪些类型可以放进 SetREADME 强调mapset v2 是generics实现可以为任何 [comparable] 类型实例化集合。Go 中的 comparable 类型包括booleans、integers、strings、floats等基本类型pointers指针arrays数组所有字段各自 comparable 的structs结构体。对应的实例化语法// Syntax example, doesnt compile. mySet : mapset.NewSet[T]() // where T is some concrete comparable type. // Therefore this code creates an int set mySet : mapset.NewSet[int]() // Or perhaps you want a string set mySet : mapset.NewSet[string]() type myStruct struct { name string age uint8 } // Alternatively a set of structs mySet : mapset.NewSet[myStruct]() // Lastly a set that can hold anything using the any or empty interface keyword: interface{}. // This effectively removes type safety. mySet : mapset.NewSet[any]()接口层面的约束可以从 set.go 直接看到type Set[T comparable] interface { // Add adds an element to the set. Returns whether // the item was added. Add(val T) bool // ...下文完整列举 }也就是说T被类型参数约束为comparable底层实现是哈希容器slice、map、func这类不可比较类型无法作为元素使用。any即空接口可以作为逃生通道持有任意值但正如 README 所说这effectively removes type safety应尽量显式指定具体类型。3. 构造函数全景线程安全是默认性能优先可显式关闭set.go 提供了 6 个构造函数覆盖带初始元素 / 指定容量 / 从 map 键构造 × 线程安全 / 非线程安全的组合构造函数签名节选线程安全适用场景NewSetNewSetT comparable Set[T]是通用场景可直接给初始元素NewSetWithSizeNewSetWithSizeT comparable Set[T]是已知元素规模预分配容量NewThreadUnsafeSetNewThreadUnsafeSetT comparable Set[T]否单线程/调用方自行加锁追求略高的速度NewThreadUnsafeSetWithSizeNewThreadUnsafeSetWithSizeT comparable Set[T]否同上预分配容量NewSetFromMapKeysNewSetFromMapKeysT comparable, V any Set[T]是从已有 map 的键批量建集合NewThreadUnsafeSetFromMapKeysNewThreadUnsafeSetFromMapKeysT comparable, V any Set[T]否同上几个值得注意的实现细节同样来自 set.goNewSet内部调用newThreadSafeSetWithSizeT)预分配容量再逐个Add因此初始元素不会触发多次扩容NewSetFromMapKeys先按len(val)分配容量再遍历 map 键Add正好对应 README 提到的 2.2.0 版本新增的从现有 Map 创建新 set的便捷能力。README 宣称的两个核心特性在此体现得最清楚一个通用接口覆盖两种实现Set[T]是两者的公共抽象调用方拿到的就是接口类型非线程安全实现偏向性能线程安全实现偏向并发使用。4.Set[T comparable]接口一次掌握全部操作以下是 set.go 中接口的完整方法清单按语义分组整理4.1 元素操作会修改集合本身方法返回值说明Add(val T)bool添加元素返回是否真正新增已存在则返回 falseAppend(val ...T)int批量追加返回实际新增的元素个数Remove(i T)-删除单个元素RemoveAll(i ...T)-批量删除Clear()-清空留下空集Pop()(T, bool)删除并返回一个任意元素空集时返回零值与 false4.2 成员判断只读方法返回值说明Contains(val ...T)bool所有给定元素都存在于集合中才为 trueContainsOne(val T)bool单个元素是否存在ContainsAny(val ...T)bool至少一个给定元素存在即为 trueIsEmpty()bool是否为空集Cardinality()int集合元素个数4.3 集合代数返回新集合不修改接收者方法说明Union(other Set[T])并集两个集合的所有元素Intersect(other Set[T])交集同时存在于两个集合的元素Difference(other Set[T])差集只在本集合、不在 other 中的元素SymmetricDifference(other Set[T])对称差在两个集合中恰好出现一次的元素接口注释中多次强调这些跨集合运算的参数必须与接收者是同一实现否则运行时 panic。也就是说不能把一个NewSet创建的线程安全 set 与一个NewThreadUnsafeSet创建的 set 直接做Union——接口约束保证元素类型T一致但实现类型一致要靠调用方自己保证。4.4 子集/超集关系与相等性方法说明Equal(other Set[T])基数相同且元素相同则相等与添加顺序无关IsSubset(other Set[T])/IsSuperset(other Set[T])子集/超集判定IsProperSubset/IsProperSuperset真子集/真超集额外要求两集合不相等4.5 迭代、转换与序列化方法说明Each(func(T) bool)遍历并对每个元素执行回调回调返回 true 时立即停止Iter()返回-chan T可直接for elem : range set.Iter()Iterator()返回*Iterator[T]支持Stop()提前终止见 iterator.goClone()用相同实现克隆集合ToSlice()转成[]TString()Set{a, b, c}形式的字符串表示MarshalJSON/UnmarshalJSONJSON 数组形式的双向序列化5. 实战示例从 README 的完整用例看典型用法README 的 Comprehensive Example 用一个选课场景演示了 set 如何减少样板代码。完整继承如下package main import ( fmt mapset github.com/deckarep/golang-set/v2 ) func main() { // Create a string-based set of required classes. required : mapset.NewSet[string]() required.Add(cooking) required.Add(english) required.Add(math) required.Add(biology) // Create a string-based set of science classes. sciences : mapset.NewSet[string]() sciences.Add(biology) sciences.Add(chemistry) // Create a string-based set of electives. electives : mapset.NewSet[string]() electives.Add(welding) electives.Add(music) electives.Add(automotive) // Create a string-based set of bonus programming classes. bonus : mapset.NewSet[string]() bonus.Add(beginner go) bonus.Add(python for dummies) }1创建所有不重复课程的集合all : required .Union(sciences) .Union(electives) .Union(bonus) fmt.Println(all)Set{cooking, english, math, chemistry, welding, biology, music, automotive, beginner go, python for dummies}注意两点一是集合自动去重biology同时出现在required和sciences中并集里只出现一次二是Union返回新集合而不是原地修改链式调用读起来像函数式管道。2成员判断cooking 算不算理科result : sciences.Contains(cooking) fmt.Println(result)false3差集显示所有非理科课程notScience : all.Difference(sciences) fmt.Println(notScience)Set{ music, automotive, beginner go, python for dummies, cooking, english, math, welding }4交集哪些理科课同时是必修课reqScience : sciences.Intersect(required)Set{biology}5基数有多少门加试课fmt.Println(bonus.Cardinality())2从这些示例可以提炼出使用守则集合是无序的输出顺序不保证String()的打印顺序依赖 map 内部迭代序不要依赖打印结果中的元素顺序做断言需要确定性顺序时请配合第 7.2 节的Sorted。6. 源码级实现剖析map[T]struct{} 与 RWMutex6.1 非线程安全实现map[T]struct{}threadunsafe.go 的核心定义极其朴素type threadUnsafeSet[T comparable] map[T]struct{}元素只作为 map 的键存在值统一用零大小的struct{}占位因此内存开销几乎只有哈希表本身。几个关键方法的实现逻辑Add记录prevLen : len(s)写入键后返回prevLen ! len(s)从而用一次长度比较同时完成插入和是否已存在的判断threadunsafe.goAppend批量写入后返回len(s) - prevLen即真正新增的个数threadunsafe.goIntersect有一个小优化比较两个集合的基数后遍历较小的那个逐个在较大集合里做成员判断threadunsafe.go把比较次数压到 O(min(|A|,|B|))Union按max(|A|, |B|)预分配 map 容量再合并两个集合threadunsafe.goPop利用 map 迭代直接取出并删除第一个碰到的元素返回(item, true)空集返回零值与falsethreadunsafe.goClear的源码注释指出for key : range s { delete(s, key) }这种写法会被编译器优化为运行时mapclear()并不是逐个删除。此外非线程安全实现还内置了 JSON 序列化MarshalJSON把每个元素序列化后用[...]拼成 JSON 数组threadunsafe.goUnmarshalJSON把 JSON 数组解码为[]T后整体Append回来源码注释特别说明它只解码基本类型、数字按json.Number处理。6.2 线程安全实现包装而非重写threadsafe.go 展示了组合优于重复的设计type threadSafeSet[T comparable] struct { sync.RWMutex uss threadUnsafeSet[T] }线程安全版本不重新实现任何集合逻辑而是内嵌sync.RWMutex并委托给内部的threadUnsafeSetuss写操作Add、Append、Remove、RemoveAll、Clear、Pop用Lock()/Unlock()独占读操作Contains*、Cardinality、Each、ToSlice等用RLock()/RUnlock()允许并发读跨集合运算Union/Intersect/Difference/SymmetricDifference/Equal/IsSubset会先做类型断言other.(*threadSafeSet[T])再同时持有两个集合的读锁把运算下沉到内部uss完成后包装回*threadSafeSet返回threadsafe.go。这也解释了前文提到的跨实现运算会 panic类型断言失败即 panic。从源码结构看这种锁 委托的包装让两套实现共享 100% 的运算逻辑README 所称的one common interface to both implementations 正是这个结构带来的。6.3 迭代器channel 驱动 可提前停止iterator.go 定义type Iterator[T comparable] struct { C -chan T stop chan struct{} }Iterator()方法会启动一个 goroutine把 map 元素依次推入 channelStop()关闭stopchannel 并抽干剩余元素以让生产 goroutine 退出且内部recover()保证了Stop()可以被安全地多次调用。两种实现threadunsafe.go、threadsafe.go的Iterator()都在select中监听stopCh收到停止信号即跳出遍历。7. 进阶能力Sorted、JSON 序列化与迭代选择7.1Sorted把集合变成有序切片sorted.go 提供了一个包级函数// Sorted returns a sorted slice of a set of any ordered type in ascending order. // When sorting floating-point numbers, NaNs are ordered before other values. func SortedE cmp.Ordered []E { s : set.ToSlice() slices.Sort(s) return s }注意它的构建标签//go:build go1.21——依赖标准库slices/cmp仅当以 Go 1.21 构建时可用元素类型还需满足cmp.Ordered数字、字符串等可全序比较的类型比comparable更严格例如结构体不能用。Harvester 的构建工具链为 Go 1.26见 go.mod该函数在仓库构建语境下是可用的。7.2 JSON 序列化由于两种实现都实现了MarshalJSON/UnmarshalJSON线程安全版本加锁后委托给ussSet[T]可以直接交给encoding/json处理被序列化成一个 JSON 数组反序列化时只支持基本类型元素。7.3 迭代方式怎么选Each(cb)最轻量回调返回true提前结束适合查找是否存在某类元素这类短路逻辑Iter()返回一次性 channelfor循环消费写法最 Go 化Iterator()当你可能提前放弃遍历时用它Stop()能显式回收内部 goroutine。8. Harvester 源码中的真实用法mapset 并不是孤立存在——Harvester 主仓与它 vendored 的网络控制器依赖都直接使用它且用得很克制典型模式是先建一个 string 集合做去重/成员判断用完转回 slice 或只查不存。8.1 Webhook Setting 校验器VLAN 配置必须覆盖全部节点pkg/webhook/resources/setting/validator.go 中的checkVCSpansAllNodes在用户修改集群网络相关 Setting 时校验指定 cluster network 的所有 VlanConfig 的 matched-nodes 并集是否覆盖集群里的每个节点matchedNodes : mapset.NewSet[string]() vcs, err : v.vcCache.List(labels.Set{ utils.KeyClusterNetworkLabel: config.ClusterNetwork, }.AsSelector()) // ... 省略 err 与 len(vcs)0 的报错处理 ... //There will be atleast 1 vlanconfig with non-empty matched nodes, // else it will be caught by checkVlanStatusReady for _, vc : range vcs { vnodes, err : getMatchNodes(vc) if err ! nil { return err } matchedNodes.Append(vnodes...) } nodes, err : v.nodeCache.List(labels.Everything()) // ... 省略 err 处理 ... //check if vlanconfig contains all the nodes in the cluster for _, node : range nodes { //skip witness nodes which do not run LH Pods if util.IsWitnessNodeWithoutPromotionStatus(node) { continue } if !matchedNodes.Contains(node.Name) { return fmt.Errorf(vlanconfig does not span %s, node.Name) } }这里 mapset 的价值很具体多来源去重每个 VlanConfig 的 annotationKeyMatchedNodes由 getMatchNodes 从 JSON 解出[]string都Append(vnodes...)进同一集合多个 VlanConfig 覆盖同一批节点时自动去重——手写 slice 则必须自己写 O(n²) 的去重循环O(1) 成员判断对每个节点执行matchedNodes.Contains(node.Name)是哈希查找而不是线性扫描实现选择这段逻辑运行在 webhook 的单请求处理流程内没有跨 goroutine 共享这个集合使用默认的线程安全版本没有任何功能损失体现了拿不准就选默认安全实现的稳妥策略。8.2 网络控制器批量生成名称先去重Harvester 的 vendor 树中vendor/github.com/harvester/harvester-network-controller/pkg/utils/vm.go 与 vendor/github.com/harvester/harvester-network-controller/pkg/utils/vmi.go 采用同一种一行式写法// vm.go收集 VM 名称 return mapset.NewSetstring...).ToSlice(), nil // vmi.go收集 VMI 名称 return mapset.NewSetstring...).ToSlice(), nil把可能含重复项的名称列表丢进NewSetstring再ToSlice()取回——一次进集合出切片完成去重。这是 mapset 最经济实惠的用法不需要任何集合运算只用去重语义。8.3 从这两个用例归纳的选型经验去重NewSetTToSlice()或NewSetFromMapKeys(map)批量合并多个来源循环Append它返回的新增个数也便于统计覆盖性/缺失检查建好集合后逐个Contains单线程上下文中不必为省锁刻意选NewThreadUnsafeSet——Harvester 现有代码全部使用默认线程安全构造函数可读性与安全性优先只有基准测试证明锁成为瓶颈时再切换。9. 使用守则与易错点把 README 与源码结合起来可以把注意事项浓缩为元素必须 comparableslice/map/func不能作为T确有需要时用NewSet[any]()但类型安全随之消失跨集合运算返回新集合Union/Intersect/Difference/SymmetricDifference均不可变地产生新 setAdd/Remove/RemoveAll/Clear/Pop才是原地修改同一运算的两个操作数必须来自同一构造函数家族都用NewSet*或都用NewThreadUnsafeSet*否则线程安全实现里的类型断言会 panic无序性String()、ToSlice()、Iter()的顺序都不受保证需要稳定顺序时Go 1.21 构建用Sorted其元素类型需满足cmp.OrderedPop是任意取一个而非取最小/最早元素空集返回零值与falseUnmarshalJSON仅解码基本类型数字按json.Number复杂结构体元素不要指望 JSON 往返提前结束迭代请用Iterator()的Stop()它可被多次调用且会抽干 channel不会泄漏生产 goroutine在 Harvester 仓库内的使用方式依赖已固定在 go.modv2.6.0并 vendored 到 vendor/github.com/deckarep/golang-set/v2/直接import mapset github.com/deckarep/golang-set/v2即可升级该依赖属于仓库维护流程本文不展开。10. 小结mapset 的设计可以概括为三句话用map[T]struct{}做最小化存储用一个Set[T comparable]泛型接口统一抽象用RWMutex装饰出线程安全变体在 API 层则尽量对齐 Python set 的语义Add/Contains/Difference/Intersect/Cardinality……并补充了Append、ContainsOne/ContainsAny、Pop、JSON 序列化等 Go 生态里实用的扩展。在 Harvester 这类 Kubernetes 基础设施项目中它承担的是轻量去重与成员判断的角色——webhook 设置校验器 的节点覆盖性检查和网络控制器的名称去重vm.go都是小而典型的用例。如果你在自己的 Go 代码里同样需要不要重复、快速判断、集合运算mapset.NewSetstring就是一个从 README 示例即可直接复制起步的方案。赞分享云原生虚拟化存储后端高可用【免费下载链接】harvesterOpen source hyperconverged infrastructure (HCI) software项目地址https://gitcode.com/gh_mirrors/ha/harvester点击查看免费下载相关推荐golang-set泛型实战高效集合操作的进阶指南golang set泛型实战高效集合操作的进阶指南 在Go语言开发中集合操作是数据处理的核心需求之一。golang set作为经过实战检验的泛型集合库为开开发工具Upscayl 批量放大完整指南6 步自动处理上千张图片Upscayl 批量放大完整指南6 步自动处理上千张图片 手头有 500 张产品图要放大时逐张手动操作只会让人疲惫。免费开源的 AI 图像放大工具 Upsc桌面应用AI 应用计算机视觉图像处理OpenCloud 项目中的 golang-setmapset集合库实战指南API 全解、双实现机制与源码级原理OpenCloud 项目中的 golang setmapset集合库实战指南API 全解、双实现机制与源码级原理 导读 本指南以 OpenCloud 仓库后端微服务存储认证鉴权创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考