Go 中获取两个切片交集的 6 种方法

简介: 本文系统梳理 Go 中切片交集的 6 种实现:从基础双重循环、Map 查表、排序双指针,到泛型通用版、结构体自定义键、多集交集。涵盖性能对比、去重策略与实战选型建议,助你写出高效、健壮、工业级代码。(239字)

在 Go 开发中,切片交集(Intersection) 是高频需求:用户标签匹配、权限校验、数据去重同步……
本文系统梳理从基础到高阶的 6 种实现方式,含泛型、结构体、去重、性能对比,助你写出工业级健壮代码。


🔹 基础方法回顾

1. 双重循环法(O(n²),简单但低效)

func intersectLoop[T comparable](a, b []T) []T {
   
    var res []T
    for _, x := range a {
   
        for _, y := range b {
   
            if x == y {
   
                res = append(res, x)
                break // 防止重复添加同一元素(若 a 有重复)
            }
        }
    }
    return res
}

✅ 优点:无额外内存,逻辑直观
❌ 缺点:n=10⁴ 时性能急剧下降


2. Map 查表法(O(n+m),推荐通用解)

func intersectMap[T comparable](a, b []T) []T {
   
    set := make(map[T]struct{
   }, len(a))
    for _, v := range a {
   
        set[v] = struct{
   }{
   }
    }

    var res []T
    for _, v := range b {
   
        if _, ok := set[v]; ok {
   
            res = append(res, v)
            delete(set, v) // ← 关键:防止 b 中重复导致结果重复
        }
    }
    return res
}

⚠️ 注意:

  • 默认保留 b 中元素顺序
  • delete 可实现结果去重(若只需首次匹配)
  • 若需保留所有重复(多集交集),则不要 delete

3. 排序双指针法(O(n log n),内存友好)

import "sort"

func intersectSorted[T comparable](a, b []T) []T {
   
    // 创建副本避免修改原 slice
    aa, bb := make([]T, len(a)), make([]T, len(b))
    copy(aa, a)
    copy(bb, b)

    sort.Slice(aa, func(i, j int) bool {
    return aa[i] < aa[j] })
    sort.Slice(bb, func(i, j int) bool {
    return bb[i] < bb[j] })

    var res []T
    i, j := 0, 0
    for i < len(aa) && j < len(bb) {
   
        switch {
   
        case aa[i] == bb[j]:
            res = append(res, aa[i])
            i++
            j++
        case aa[i] < bb[j]:
            i++
        default:
            j++
        }
    }
    return res
}

✅ 适合大内存受限场景(如嵌入式)
❌ 破坏原顺序,且 T 必须可排序


🔥 高级方法

4. 【泛型通用版】交集函数(Go 1.18+)

结合 map 法 + 泛型,写出生产级工具函数

// Intersection returns the intersection of two slices.
// Result preserves the order of elements in `a`, and removes duplicates.
// T must be comparable.
func Intersection[T comparable](a, b []T) []T {
   
    if len(a) == 0 || len(b) == 0 {
   
        return nil
    }

    // Step 1: Build lookup set from b (smaller one for memory efficiency)
    if len(b) < len(a) {
   
        a, b = b, a // ensure `a` is the smaller slice
    }
    set := make(map[T]struct{
   }, len(a))
    for _, v := range a {
   
        set[v] = struct{
   }{
   }
    }

    // Step 2: Iterate b, collect matches while preserving order & dedup
    seen := make(map[T]struct{
   }) // track emitted items
    var res []T
    for _, v := range b {
   
        if _, inA := set[v]; inA {
   
            if _, emitted := seen[v]; !emitted {
   
                res = append(res, v)
                seen[v] = struct{
   }{
   }
            }
        }
    }
    return res
}

✅ 优势:

  • 自动选小切片建 map → 节省内存
  • 保留 b 的出现顺序
  • 天然去重(结果每个元素唯一)
  • 泛型支持 int, string, struct{}(只要 comparable

▶️ Go Playground 示例


5. 【结构体交集】自定义等价判断

当元素是结构体,且仅部分字段需参与比较时(如 User{ID, Name} 只按 ID 比较):

type User struct {
   
    ID   int
    Name string
}

// Key returns a comparable key for equality comparison
func (u User) Key() int {
    return u.ID }

// IntersectionBy extracts intersection using a key function
func IntersectionBy[T any, K comparable](a, b []T, key func(T) K) []T {
   
    keySet := make(map[K]struct{
   })
    for _, v := range a {
   
        keySet[key(v)] = struct{
   }{
   }
    }

    var res []T
    seen := make(map[K]struct{
   })
    for _, v := range b {
   
        k := key(v)
        if _, ok := keySet[k]; ok {
   
            if _, dup := seen[k]; !dup {
   
                res = append(res, v)
                seen[k] = struct{
   }{
   }
            }
        }
    }
    return res
}

// Usage
usersA := []User{
   {
   1, "Alice"}, {
   2, "Bob"}, {
   3, "Carol"}}
usersB := []User{
   {
   2, "Bob2"}, {
   4, "David"}, {
   1, "Alice2"}}

common := IntersectionBy(usersA, usersB, User.Key)
// → [{1 "Alice"}, {2 "Bob"}] (按 ID 匹配,保留 usersB 中的完整结构)

💡 适用场景:

  • 数据库记录对比(按 ID)
  • API 响应去重(按唯一键)
  • 忽略时间戳/版本字段的结构体同步

6. 【多集交集】保留重复次数的交集

若需数学意义上的多重集交集(如 A=[1,1,2], B=[1,2,2] → 交集为 [1,2],各取 min(count)):

func IntersectionMultiset[T comparable](a, b []T) []T {
   
    // Count frequency in a
    freqA := make(map[T]int)
    for _, v := range a {
   
        freqA[v]++
    }

    // For each in b, take min(count)
    var res []T
    for _, v := range b {
   
        if cnt, ok := freqA[v]; ok && cnt > 0 {
   
            res = append(res, v)
            freqA[v]-- // consume one occurrence
        }
    }
    return res
}

// Example:
a := []int{
   1, 1, 2, 3}
b := []int{
   1, 2, 2, 4}
fmt.Println(IntersectionMultiset(a, b)) // → [1, 2]

📊 性能对比( 10k 元素 string slice)

goos: linux
goarch: amd64
pkg: example
cpu: Intel(R) Core(TM) i7-10700K
BenchmarkIntersectLoop-16               3     452,123,840 ns/op
BenchmarkIntersectMap-16             1000       1,021,344 ns/op
BenchmarkIntersectSorted-16          1000       1,241,892 ns/op
BenchmarkIntersectionGeneric-16      1000       1,045,217 ns/op

✅ 结论:

  • Map 法/泛型通用版 最快(~1ms
  • 循环法慢 400+ 倍,仅适用于 n < 100
  • 排序法略慢于 map,但内存稳定(无 hash 表开销)

📌 实际建议:优先用 Intersection[T comparable] 泛型函数,兼顾性能、可读性、去重、顺序。


🎯 实战建议

场景 推荐方案
快速原型 / 小数据(<100) 双重循环 + break
通用场景(string/int) 泛型 Intersection[T]
结构体按字段比较 IntersectionBy + key 函数
需保留重复次数(多集) IntersectionMultiset
内存极其受限 排序双指针法
需要稳定顺序 + 去重 泛型版(内置顺序+去重)

相关文章
|
14天前
|
Linux 开发工具 C++
可以完全扔掉鼠标了!在终端爽用VS Code的完全技巧(Windows/Mac/Linux)
本文讲述程序员为守护“心流”而封印鼠标,通过命令行一键启动VS Code的实践指南。详解Windows/macOS/Linux三端配置`code`命令的方法,并揭秘`-g`(跳转行)、`-d`(文件对比)、`-w`(Git等待)等高效参数,让开发体验更连贯、自由。
100 0
|
14天前
|
Rust 安全 程序员
世界上最懒的语言,八年:Zig为何迟迟不推出1.0版本
Zig是一门追求“永不后悔设计”的系统编程语言,由Andrew Kelley独立开发8年仍坚守1.0门槛。它拒绝为短期热度妥协,以非营利模式深耕语言根基:比C更安全简洁,比Rust更轻量可控。其“战略性不发布”,实为对承诺的极致敬畏。(239字)
118 0
|
14天前
|
人工智能 前端开发 Shell
用完Zed之后,我发现了VS Code 和Zed的一个重大区别
作者尝试Zed等轻量编辑器后,发现VS Code真正的优势在于“工作流连续性”:自动恢复终端历史、工作目录、分屏布局与命令上下文,极大降低心智负担。这并非功能堆砌,而是将终端深度融入开发环境的状态管理——轻快易用与环境记忆之间,开发者终需权衡。(239字)
103 0
|
10月前
|
存储 人工智能 API
阿里 Qoder 新升级,Repo Wiki 支持共享、编辑和导出
Qoder 是阿里巴巴推出的 Agentic 编程平台,集成顶尖编程模型,具备强大上下文工程能力,可一次检索10万代码文件,大幅提升软件开发效率。其 Repo Wiki 功能自动生成结构化文档,支持共享、编辑与导出,助力知识沉淀与团队协作。
|
Prometheus Cloud Native Go
Go 1.22 标准库 slices 新增函数和一些旧函数增加新特性
Go 1.22 标准库 slices 新增函数和一些旧函数增加新特性
|
消息中间件 负载均衡 Java
如何设计一个分布式配置中心?
这篇文章介绍了分布式配置中心的概念、实现原理及其在实际应用中的重要性。首先通过一个面试场景引出配置中心的设计问题,接着详细解释了为什么需要分布式配置中心,尤其是在分布式系统中统一管理配置文件的必要性。文章重点分析了Apollo这一开源配置管理中心的工作原理,包括其基础模型、架构模块以及配置发布后实时生效的设计。此外,还介绍了客户端与服务端之间的交互机制,如长轮询(Http Long Polling)和定时拉取配置的fallback机制。最后,结合实际工作经验,分享了配置中心在解决多台服务器配置同步问题上的优势,帮助读者更好地理解其应用场景和价值。
818 18
|
关系型数据库 Go 数据处理
高效数据迁移:使用Go语言优化ETL流程
在本文中,我们将探索Go语言在处理大规模数据迁移任务中的独特优势,以及如何通过Go语言的并发特性来优化数据提取、转换和加载(ETL)流程。不同于其他摘要,本文不仅展示了Go语言在ETL过程中的应用,还提供了实用的代码示例和性能对比分析。
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
1398 1
|
JSON 搜索推荐 Go
ZincSearch搜索引擎中文文档及在Go语言中代码实现
ZincSearch官网及开发文档均为英文,对非英语用户不够友好。GoFly全栈开发社区将官方文档翻译成中文,并增加实战经验和代码,便于新手使用。本文档涵盖ZincSearch在Go语言中的实现,包括封装工具库、操作接口、统一组件调用及业务代码示例。官方文档https://zincsearch-docs.zinc.dev;中文文档https://doc.goflys.cn/docview?id=41。
725 0