← 中文资源

简体中文版本

保持顺序的切片去重

用泛型和集合去掉可比较值中的重复项,保留首次出现顺序,并返回新切片以免修改调用方数据。

阅读英文原文

功能说明

去重是数据清洗和接口聚合中的常见步骤。若只关心成员关系,直接把元素放入 map 再取出即可,但 map 遍历顺序随机,结果不可复现;若在原切片上原地覆盖,又会悄悄修改调用方持有的底层数组,容易引发难以排查的副作用。本例使用泛型约束 comparable,返回全新切片,按首次出现顺序保留元素。

实现只需要一次遍历:每遇到一个值,先查集合,已存在就跳过;否则加入集合并追加到结果。时间复杂度为线性,空间与唯一元素数量成正比。main 用包含重复字符串的切片演示,并打印原切片确认未被修改。

关键代码

Unique 的签名使用类型参数 T 并约束为 comparable,使字符串、整数、指针和普通结构体都能使用,同时排除切片、映射和函数等不可比较类型。map[T]struct{} 只表达存在性,空结构体不占额外存储。结果用 make 预分配容量,避免追加过程中多次扩容。循环中的 exists 判断与写入分离,逻辑清晰。main 同时打印去重结果与原始输入,便于观察不可变性。

使用注意

comparable 结构体按所有字段比较,包含浮点 NaN 时行为特殊,NaN 不等于自身,可能保留多个副本。若需要按某个业务键去重,应使用自定义函数而不是整体比较。该实现不排序,保留首次出现顺序;需要稳定排序时可在去重后另行排序。返回新切片会额外占用内存,数据量极大时可改为原地算法,但必须让调用方明确知晓输入会被修改。并发调用时每个调用应使用独立的 seen 映射。

验证方式

运行脚本,第一行输出去重结果 a、b、c,顺序与首次出现一致;第二行原始切片仍为 a、b、a、c、b,证明输入未被改写。再把元素改成整数或自定义结构体,代码无需修改即可编译,体现泛型复用能力。

代码示例

package main

import "fmt"

func Unique[T comparable](values []T) []T {
	seen := make(map[T]struct{}, len(values))
	result := make([]T, 0, len(values))
	for _, value := range values {
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func main() {
	values := []string{"a", "b", "a", "c", "b"}
	unique := Unique(values)
	fmt.Println(unique)
	fmt.Println(values)
}