登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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)
}
slices.CompactFunc 比较函数、相邻事件和保留首项关系的静态结构说明图
图1:比较规则如何连接事件字段、相邻运行和保留项的结构说明图,不是运行截图。

这个输入最后会留下 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)
}
slices.CompactFunc 相邻重复、非相邻重复、nil 切片和尾部清零边界关系图
图2:相邻压缩与非相邻保留、nil 语义、尾部清零之间的边界关系说明图,不是运行截图。
检查项应观察到的结果
连续三条业务相等记录只保留第一条,长度减少两项
相等记录被其他事件隔开两条都保留,不做全局去重
nil 切片结果仍保持 nil 语义
指针或大对象尾部新长度之外的元素被清零

迁移清单和常见追问

迁移可以按这个顺序完成:确认项目使用 Go 1.21 或更高版本;把原循环的“相同”条件提取成 eq;明确只处理相邻运行;接住返回切片;补上连续、非相邻、nil 和尾部引用测试。完成后再删除旧循环,避免两套实现同时改变业务。

CompactFunc 会删除所有相同元素吗?不会,它只压缩连续且被 eq 判定为相同的一段。

为什么一定要接收返回值?返回切片的长度变短了,后续遍历必须使用新长度。

比较函数可以只比较一个字段吗?可以,但那个字段必须确实代表业务身份;否则会把不同租户、渠道或状态的记录误合并。

把 slices.CompactFunc 当作“带业务比较器的相邻压缩”最稳妥:它能替代重复的读写下标代码,但不会替你决定全局去重、字段身份或对象生命周期。迁移时守住这三个边界,标准库写法才会和旧业务行为保持一致。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>