Go slices.CompactFunc 怎样按业务规则去除相邻重复项
来源:17golang原创
时间:2026-10-25 19:26:35 270浏览 收藏
如果旧代码只是把连续出现、且满足某个业务条件的记录压缩成一条,可以把手写循环迁移到 slices.CompactFunc。它不会做全局去重,而是按顺序比较当前保留下来的最后一项与后续元素;比较函数返回 true 时保留这一组的第一项。
官方地址:https://pkg.go.dev/slices#CompactFunc
这次迁移的关键不是把函数名换掉,而是保留原业务语义:只消除相邻重复、比较什么由 eq 决定、结果要接住返回值,而且原切片内容会被原地改写。slices 是 Go 1.21 引入的标准库包;低于这个版本的项目需要先处理工具链升级或继续保留旧实现。
升级范围:从手写循环迁移到标准库
Go 1.21 增加了标准库 slices,其中的 CompactFunc 签名是 func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S。它适合元素不一定支持 ==、但可以由业务函数判断“相等”的切片。
| 迁移点 | 新行为 | 需要回归的风险 |
|---|---|---|
| 相等判断 | 由 eq(a, b) 决定 | 比较范围不能误删业务字段 |
| 去重范围 | 只压缩连续重复运行 | 非相邻相同值仍然保留 |
| 内存行为 | 修改原切片并返回较短切片 | 调用方必须使用返回值 |
| 尾部处理 | 新长度之外的元素会被清零 | 不能继续把旧长度当有效数据 |
因此,使用前先确认模块的 go 版本和部署环境。如果旧代码依赖“生成一个全新的切片”或“全局删除相同值”,这就不是等价替换,应先把目标行为写进回归用例。
新写法:让比较函数表达业务规则
下面用事件记录表示一个常见迁移场景:同一渠道中,名称大小写不同但业务上视为同一事件;只有连续出现时才合并,后面的非相邻事件不能被全局删除。
package main
import (
"fmt"
"slices"
"strings"
)
type Event struct {
Name string
Channel string
}
func main() {
events := []Event{
{Name: "Login", Channel: "web"},
{Name: "login", Channel: "web"},
{Name: "Pay", Channel: "web"},
{Name: "LOGIN", Channel: "web"},
}
// 只把名称忽略大小写且渠道相同的相邻事件视为重复。
events = slices.CompactFunc(events, func(a, b Event) bool {
return strings.EqualFold(a.Name, b.Name) && a.Channel == b.Channel
})
// CompactFunc 返回压缩后的切片,必须用它更新后续要读取的长度。
fmt.Println(events)
}

这个输入最后会留下 Login/web、Pay/web、LOGIN/web 三项。第三项虽然和第一项业务上相等,但它们之间隔着 Pay,所以不会被删除。比较函数越窄,迁移越安全;不要为了“去得更干净”把它改成全局扫描。
旧代码风险:不要漏掉返回值和原地语义
手写循环常常同时维护读写下标,迁移时最容易漏掉两件事:第一,CompactFunc 返回的是新长度对应的切片;第二,它可能复用原底层数组。只调用函数但继续读取原来的 len(events),会把已经不属于结果的尾部当成有效记录。
func compactEvents(events []Event) []Event {
// 空切片直接返回,保留 nil 切片的语义。
if len(events) == 0 {
return events
}
// 返回值代表有效范围;函数内部会清理新长度之外的元素。
return slices.CompactFunc(events, func(a, b Event) bool {
return strings.EqualFold(a.Name, b.Name) && a.Channel == b.Channel
})
}
如果切片元素包含指针、映射或较大的对象引用,尾部清零尤其值得注意:结果之外的引用不会继续挂在底层数组上。若结果需要长期独立保存,还要按业务需要再做复制或容量裁剪;CompactFunc 本身解决的是相邻压缩,不自动替你完成所有生命周期设计。
回归检查:相邻重复、nil 和业务边界
升级后至少检查四类输入。连续相同项应该只保留第一项;非相邻相同项必须继续存在;空切片和 nil 切片不能被业务代码误判;比较函数中的字段范围则要覆盖大小写、渠道、租户或状态等真正影响“相同”的条件。
func sameEvent(a, b Event) bool {
// 只比较业务定义的身份字段,不把展示字段误当成去重条件。
return strings.EqualFold(a.Name, b.Name) && a.Channel == b.Channel
}
func compactForTest(events []Event) []Event {
// 统一从一个入口调用,方便测试结果长度和原地修改后的内容。
return slices.CompactFunc(events, sameEvent)
}

| 检查项 | 应观察到的结果 |
|---|---|
| 连续三条业务相等记录 | 只保留第一条,长度减少两项 |
| 相等记录被其他事件隔开 | 两条都保留,不做全局去重 |
nil 切片 | 结果仍保持 nil 语义 |
| 指针或大对象尾部 | 新长度之外的元素被清零 |
迁移清单和常见追问
迁移可以按这个顺序完成:确认项目使用 Go 1.21 或更高版本;把原循环的“相同”条件提取成 eq;明确只处理相邻运行;接住返回切片;补上连续、非相邻、nil 和尾部引用测试。完成后再删除旧循环,避免两套实现同时改变业务。
CompactFunc 会删除所有相同元素吗?不会,它只压缩连续且被 eq 判定为相同的一段。
为什么一定要接收返回值?返回切片的长度变短了,后续遍历必须使用新长度。
比较函数可以只比较一个字段吗?可以,但那个字段必须确实代表业务身份;否则会把不同租户、渠道或状态的记录误合并。
把 slices.CompactFunc 当作“带业务比较器的相邻压缩”最稳妥:它能替代重复的读写下标代码,但不会替你决定全局去重、字段身份或对象生命周期。迁移时守住这三个边界,标准库写法才会和旧业务行为保持一致。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
229 收藏
-
306 收藏
-
471 收藏
-
245 收藏
-
470 收藏
-
356 收藏
-
294 收藏
-
211 收藏
-
318 收藏
-
169 收藏
-
316 收藏
-
132 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习