go切片去重无标准库函数,保序高效做法是一次遍历配合map[t]bool或map[t]struct{}标记;slices.compact仅压缩相邻重复项,需先排序且会丢失原始顺序。

Go 切片去重没有标准库函数,保序且高效的做法只有一种:一次遍历 + map[T]bool 或 map[T]struct{} 标记。泛型封装只是语法糖,底层仍是这个逻辑——别被“泛型一行”误导,slices.Compact 不是去重,它只压缩相邻重复项。
为什么不能直接用 slices.Compact
slices.Compact 要求输入已排序且重复元素必须相邻,否则完全无效。比如 []int{1, 2, 1} 经 slices.Compact 后仍是 []int{1, 2, 1},因为两个 1 不相邻。它本质是“去连续重复”,不是“去全局重复”。
- 适用场景:排序后日志行去噪、时间序列中连续相同状态合并
- 误用后果:看似调用了标准库,实际没去重,线上数据悄悄重复
- 正确组合:先
slices.Sort,再slices.Compact→ 但顺序丢失,且多一次 O(n log n) 排序
map[T]bool 和 map[T]struct{} 怎么选
二者性能一致,区别只在语义和内存:
-
map[T]bool:读起来直白,“这个值见过吗?!seen[v]”一目了然 -
map[T]struct{}:struct{}占 0 字节,10 万个 key 能省几 KB 内存(仅在极端规模下有意义) - 绝对不要用
map[T]int或map[T]string:零值干扰判断,if seen[v] == 0无法区分“未见过”和“见过但值为 0”
泛型函数 Unique[T comparable] 的真实约束
泛型不是万能钥匙,T comparable 是硬门槛:
- 支持类型:
int、string、字段全可比较的struct(如type User { ID int; Name string }) - 编译报错类型:含
slice、map、func字段的结构体,或带指针字段但指针目标不可比较 - 常见翻车点:把
*User当T传入——指针本身可比较,但去重逻辑往往要比较所指内容,此时应提取User.ID作 key - 不推荐用
reflect.DeepEqual补救:慢、难 debug、掩盖设计缺陷
结构体切片去重最容易忽略的细节
不是所有结构体都能直接塞进 map 做 key:
- 先确认结构体是否支持
==:运行go vet或直接编译,报invalid map key type就停手 - 若含不可比较字段(如
map[string]int),必须降维:提取 ID、Name 等可比较字段构造辅助 key,例如type key struct{ ID int; Group string } - 别用
json.Marshal生成字符串 key:性能差、易因字段顺序/空格/浮点精度出错 - 预分配切片容量:
result := make([]T, 0, len(src))减少扩容拷贝,尤其对大切片明显
真正卡住人的从来不是代码怎么写,而是动手前没花 10 秒看一眼结构体定义里有没有 map 或 slice 字段——编译失败比运行时数据重复好查得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











