为什么性能差异这么大最简单的写法新建一个切片把不在结果里的添加进去。func unique(arr []int) []int {result : []int{}for _, item : range arr {// 如果当前元素不在结果切片里添加进去// slices.Contains 是 O(n) 线性扫描整体则是 O(n²)if !slices.Contains(result, item) {result append(result, item)}}return result}问题在于每次Contains都要遍历一遍result复杂度是 O(n²)。优化思路换一种判重方式map / SetO(1) 查询map[T]bool、map[T]struct{}排序O(n log n)相同元素相邻后扫一遍泛型 标准库slices.Compact一行搞定位图[]uint64实现 BitSet海量非负整数极致空间效率递归换种表达方式本质还是上面几种推荐方案需求代码性能保序通用工具unique[T comparable]泛型函数O(n)✓标准库一行slices.Sort(arr); slices.Compact(arr)O(n log n)排序最快无序map[T]struct{}转切片O(n)✗海量整数[]uint64位图O(n)✓第1类基础循环方法1-6策略原理不依赖 map 或泛型纯靠下标、嵌套循环、slices.Index这种原始手段完成去重。每一步判重都是 O(n)整体 O(n²)。适用场景教学、面试手撕、嵌入式或受限环境。生产代码不建议使用。否是原切片取下一个元素结果切片是否已存在?append 追加跳过继续// 方法1双循环索引比较——i 与左侧每个 j 比对func unique1(arr []int) []int {result : make([]int, 0, len(arr))for i : 0; i len(arr); i {for j : 0; j i; j {if arr[i] arr[j] {// i j 表示前面没有相同值是首次出现if i j {result append(result, arr[i])}break}}}return result}// 方法2新建切片 slices.Contains 检查func unique2(arr []int) []int {result : make([]int, 0, len(arr))for _, item : range arr {// Contains 仍然是 O(n) 线性扫描if !slices.Contains(result, item) {result append(result, item)}}return result}// 方法3从后往前原地删除// 倒序遍历与左侧任意相同则删除自身func unique3(arr []int) []int {l : len(arr)for l 0 {l--i : lfor i 0 {i--if arr[l] arr[i] {// append 拼接相当于 splice删除下标 l 处的元素arr append(arr[:l], arr[l1:]...)break}}}return arr}// 方法4从前往后原地删除删后面相同项func unique4(arr []int) []int {l : len(arr)for i : 0; i l; i {for j : i 1; j l; j {if arr[i] arr[j] {arr append(arr[:j], arr[j1:]...)j-- // 删除后下标回退l-- // 长度同步减一}}}return arr[:l]}// 方法5slices.Index 索引判等// 首次位置等于当前下标即首次出现func unique5(arr []int) []int {result : make([]int, 0, len(arr))for i, item : range arr {if slices.Index(arr, item) i {result append(result, item)}}return result}// 方法6从右往左跳过重复// 倒序扫描遇相同则把 i 整体左移跳过这一段重复区func unique6(arr []int) []int {n : len(arr)tmp : make([]int, n)x : nfor i : n - 1; i 0; i-- {for j : i - 1; j 0; j-- {if arr[i] arr[j] {i--j i}}x--tmp[x] arr[i]}return tmp[x:]}第2类map 与 Set方法7-11策略原理Go 没有原生 Set 类型但map[T]struct{}是社区公认的 Set 惯用法——struct{} 不占内存只用键去重。把数据塞进 map去重就自然完成。map[T]bool最直观bool 值 1 字节map[T]struct{}节省内存工程首选自定义Set结构体封装 Add/Contains 接口复用性更强频次 map去重同时统计代价是元素必须可比较Go 中可比较指能用比较包含基本类型、指针、可比较的结构体等。适用场景日常项目首选。需要保序就维护一个 result 切片写入时同步推到结果尾。否是原切片逐个判重已在 map 中?哈希查询map 添加键result 追加自动忽略最后返回// 方法7map[int]bool 显式判重func unique7(arr []int) []int {seen : make(map[int]bool, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if !seen[item] {seen[item] trueresult append(result, item)}}return result}// 方法8map[int]struct{} 空值集合——Set 惯用法func unique8(arr []int) []int {seen : make(map[int]struct{}, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}// 方法9自定义 Set 结构体type IntSet struct {data map[int]struct{}}func newIntSet() *IntSet { return IntSet{data: map[int]struct{}{}} }func (s *IntSet) Add(v int) { s.data[v] struct{}{} }func (s *IntSet) Contains(v int) bool { _, ok : s.data[v]; return ok }// 方法10直接 map 转切片——写法最短但顺序随机func unique10(arr []int) []int {m : make(map[int]struct{}, len(arr))for _, item : range arr {m[item] struct{}{}}result : make([]int, 0, len(m))for k : range m {// Go 的 map 遍历顺序是随机的Go 团队故意设计的result append(result, k)}return result}// 方法11频次统计 map——去重 业务统计func unique11(arr []int) []int {count : make(map[int]int, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if count[item] 0 {result append(result, item)}count[item] // 累加频次}return result}第3类排序后去重方法12-14策略原理先sort.Ints让相同元素相邻再扫一遍删除相邻相同项。复杂度由排序决定O(n log n)。优点是不需要额外哈希结构相邻判等是最便宜的判重方式缺点是会破坏原顺序且要求元素可比较基本类型直接 OK自定义类型需要sort.Slice配合。适用场景输出本就需要排序、不在意原顺序、内存敏感。是否原切片排序相同元素相邻相邻是否相同?删后者保留结果// 方法12排序后从后往前删func unique12(arr []int) []int {sort.Ints(arr)for l : len(arr) - 1; l 0; l-- {if arr[l] arr[l-1] {arr append(arr[:l], arr[l1:]...)}}return arr}// 方法13排序后从前往后删func unique13(arr []int) []int {sort.Ints(arr)l : len(arr) - 1for i : 0; i l; i {if arr[i] arr[i1] {arr append(arr[:i], arr[i1:]...)i--l--}}return arr}// 方法14经典双指针LeetCode 26 题解法// 原地排序后在原切片上原地去重O(1) 额外空间func unique14(arr []int) []int {if len(arr) 0 {return arr}sort.Ints(arr)slow : 0for fast : 1; fast len(arr); fast {// 快指针发现新值slow 前进一步并写入if arr[fast] ! arr[slow] {slowarr[slow] arr[fast]}}return arr[:slow1]}第4类泛型与函数式方法15-17策略原理Go 1.18 引入泛型让通用工具函数成为可能Go 1.21 标准库新增slices.Compact把已排序切片相邻去重做成一行。函数式方面闭包 高阶函数也能模拟其他语言里的filter。适用场景现代 Go 工程的常态写法。可读性高、复用性好特别适合编写通用的工具库。泛型标准库高阶函数原切片选择方式unique支持任意 comparableslices.Sort Compactfilter 闭包谓词结果// 方法15泛型去重Go 1.18// comparable 约束保证 比较有效func unique15[T comparable](arr []T) []T {seen : make(map[T]struct{}, len(arr))result : make([]T, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}// 方法16标准库 slices.CompactGo 1.21// Compact 删除相邻重复元素要求已排序func unique16(arr []int) []int {slices.Sort(arr)return slices.Compact(arr)}// 方法17高阶 filter 闭包谓词func filter[T any](arr []T, pred func(T) bool) []T {result : make([]T, 0, len(arr))for _, item : range arr {if pred(item) {result append(result, item)}}return result}// 方法17高阶函数式法, 用闭包封装已存在状态模拟函数式 filterfunc unique17(arr []int) []int {seen : make(map[int]struct{}, len(arr))// 闭包捕获 seen谓词带副作用首次见到才返回 truereturn filter(arr, func(x int) bool {if _, ok : seen[x]; ok {return false}seen[x] struct{}{}return true})}第5类递归与位图方法18-20策略原理递归用自调用替代循环是函数式思维的体现主要用于教学位图用[]uint64自己实现 BitSet——每一位标记一个非负整数是否出现过对整数集合有极致的空间效率10 亿个 int 只要 128MB是大数据去重的常见选型。适用场景递归——教学、题型熟悉BitSet——大规模非负整数如用户 ID、订单号的去重统计。是否是否切片 lengthnlength 1?返回检查末尾元素是否在前面出现重复?丢弃末尾保留末尾递归 length-1// 方法18递归原地删除func unique18(arr []int, length int) []int {if length 1 {return arr}last : length - 1for i : last - 1; i 0; i-- {if arr[last] arr[i] {arr append(arr[:last], arr[last1:]...)break}}return unique18(arr, length-1)}// 方法19递归拼接返回不修改原切片纯函数式func unique19(arr []int, length int) []int {if length 1 {return []int{}}last : length - 1isRepeat : falsefor i : last - 1; i 0; i-- {if arr[last] arr[i] {isRepeat truebreak}}head : unique19(arr, length-1)if !isRepeat {head append(head, arr[last])}return head}// 方法20BitSet 位图仅适用于非负整数// 用 []uint64 自己实现位图每个 int 占一位func unique20(arr []int) []int {maxVal : 0for _, v : range arr {if v 0 {panic(BitSet 不支持负数需要先偏移)}if v maxVal {maxVal v}}bits : make([]uint64, maxVal/641)result : make([]int, 0, len(arr))for _, v : range arr {// 第 v 位为 0 表示首次出现if bits[v/64](1(v%64)) 0 {bits[v/64] | 1 (v % 64)result append(result, v)}}return result}选择指南不需要需要大量非负整数顺便要排序一般规模代码通用工程清晰按字段去重切片去重是否需要保序看数据特征保留原顺序数据规模与类型[]uint64 位图极致空间效率slices.Sort Compact标准库一行map[T]struct{} 转切片O(n) 最快侧重点unique[T comparable]泛型工具map result显式语义传入 keyFn携带业务键类别时间复杂度是否保序主要场景基础循环O(n²)是教学、面试手撕map / SetO(n)看实现日常项目首选排序后去重O(n log n)否变排序顺便要排序泛型与函数式O(n)是现代 Go 通用工具递归 / 位图视实现看实现教学 / 海量整数实际项目里怎么选绝大多数情况一个泛型函数就够// 保序、O(n)、对所有 comparable 类型有效func unique[T comparable](arr []T) []T {seen : make(map[T]struct{}, len(arr))result : make([]T, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}不在意顺序m : make(map[int]struct{})for _, v : range data {m[v] struct{}{}}result : make([]int, 0, len(m))for k : range m {result append(result, k)}需要排序slices.Sort(data)data slices.Compact(data) // Go 1.21海量非负整数// 用 []uint64 自己实现位图10 亿规模也只要 ~128MBbits : make([]uint64, maxVal/641)for _, v : range data {bits[v/64] | 1 (v % 64)}带业务逻辑的去重实际工作里经常遇到这样的情况遇到重复时不能简单丢弃要按某个规则做处理。比如按ID去重但要保留分数最高的那条记录去重的同时累加重复次数数值在某个区间内才参与去重这类需求 map 直接搞不定需要把判重和处理两步拆开来写。Go 里通常用泛型函数 合并函数// UniqueBy 带业务规则的去重。//// keyFn 从元素提取去重键。// onDup 遇到重复时如何合并 (旧值, 新值) - 新代表值。func UniqueBy[T any, K comparable](data []T,keyFn func(T) K,onDup func(old, new T) T,) []T {chosen : make(map[K]T)order : make([]K, 0)for _, item : range data {k : keyFn(item)if _, ok : chosen[k]; !ok {chosen[k] itemorder append(order, k)} else if onDup ! nil {chosen[k] onDup(chosen[k], item)}}result : make([]T, 0, len(order))for _, k : range order {result append(result, chosen[k])}return result}例 1按ID去重保留分数最高的type Student struct {ID intName stringScore int}students : []Student{{ID: 1, Name: 张三, Score: 90},{ID: 1, Name: 张三, Score: 95}, // 同 id分数更高{ID: 2, Name: 李四, Score: 85},}result : UniqueBy(students,func(s Student) int { return s.ID },func(old, new Student) Student {if new.Score old.Score {return new}return old},)// result: [{1 张三 95} {2 李四 85}]例 2去重同时统计频次counts : make(map[string]int)order : []string{}for _, item : range data {if _, ok : counts[item]; !ok {order append(order, item)}counts[item]}// order 是保序的去重结果counts 是频次统计例 3区间过滤——只对[0, 100]区间内的值去重区间外原样保留seen : make(map[int]struct{})result : []int{}for _, x : range data {if x 0 x 100 {if _, dup : seen[x]; dup {continue}seen[x] struct{}{}}result append(result, x)}这三个例子是同一种思路把判重与业务规则分开。判重用 map 保证 O(n)规则部分留给回调或显式分支处理。自定义对象去重comparable 约束Go 的可比较概念比 Java 的 equals 更严格——不是所有类型都能直接进 map 当键类型是否可比较备注基本类型int, string, bool 等✓直接可用指针✓比较指针地址数组[N]T✓元素可比较即可字段全部可比较的 struct✓逐字段比较接口类型✓但运行时 panic 风险slice / map / func✗不可比较含 slice 字段的 struct✗不可比较如果元素是含 slice 的 struct需要自己提取一个可比较的键type User struct {ID int64Name stringTags []string // 不可比较}// 用 ID 作为去重键seen : make(map[int64]struct{})result : []User{}for _, u : range users {if _, ok : seen[u.ID]; !ok {seen[u.ID] struct{}{}result append(result, u)}}注意Go 没有 equals/hashCode 重写机制。要按业务字段去重就要么自己定义 keyFn要么把业务等价压进一个可比较的 struct 字段。总结工程应用选择默认用泛型unique[T comparable](arr []T) []T保序、一行、O(n)标准库一步到位slices.Sortslices.Compact原地、O(n log n)、顺便排序不要顺序就直接map[T]struct{}转切片海量非负整数用[]uint64自实现位图含 slice 字段的 struct 先提取可比较键再去重
Go数组去重的20种实现方式,用不同思路解决问题
为什么性能差异这么大最简单的写法新建一个切片把不在结果里的添加进去。func unique(arr []int) []int {result : []int{}for _, item : range arr {// 如果当前元素不在结果切片里添加进去// slices.Contains 是 O(n) 线性扫描整体则是 O(n²)if !slices.Contains(result, item) {result append(result, item)}}return result}问题在于每次Contains都要遍历一遍result复杂度是 O(n²)。优化思路换一种判重方式map / SetO(1) 查询map[T]bool、map[T]struct{}排序O(n log n)相同元素相邻后扫一遍泛型 标准库slices.Compact一行搞定位图[]uint64实现 BitSet海量非负整数极致空间效率递归换种表达方式本质还是上面几种推荐方案需求代码性能保序通用工具unique[T comparable]泛型函数O(n)✓标准库一行slices.Sort(arr); slices.Compact(arr)O(n log n)排序最快无序map[T]struct{}转切片O(n)✗海量整数[]uint64位图O(n)✓第1类基础循环方法1-6策略原理不依赖 map 或泛型纯靠下标、嵌套循环、slices.Index这种原始手段完成去重。每一步判重都是 O(n)整体 O(n²)。适用场景教学、面试手撕、嵌入式或受限环境。生产代码不建议使用。否是原切片取下一个元素结果切片是否已存在?append 追加跳过继续// 方法1双循环索引比较——i 与左侧每个 j 比对func unique1(arr []int) []int {result : make([]int, 0, len(arr))for i : 0; i len(arr); i {for j : 0; j i; j {if arr[i] arr[j] {// i j 表示前面没有相同值是首次出现if i j {result append(result, arr[i])}break}}}return result}// 方法2新建切片 slices.Contains 检查func unique2(arr []int) []int {result : make([]int, 0, len(arr))for _, item : range arr {// Contains 仍然是 O(n) 线性扫描if !slices.Contains(result, item) {result append(result, item)}}return result}// 方法3从后往前原地删除// 倒序遍历与左侧任意相同则删除自身func unique3(arr []int) []int {l : len(arr)for l 0 {l--i : lfor i 0 {i--if arr[l] arr[i] {// append 拼接相当于 splice删除下标 l 处的元素arr append(arr[:l], arr[l1:]...)break}}}return arr}// 方法4从前往后原地删除删后面相同项func unique4(arr []int) []int {l : len(arr)for i : 0; i l; i {for j : i 1; j l; j {if arr[i] arr[j] {arr append(arr[:j], arr[j1:]...)j-- // 删除后下标回退l-- // 长度同步减一}}}return arr[:l]}// 方法5slices.Index 索引判等// 首次位置等于当前下标即首次出现func unique5(arr []int) []int {result : make([]int, 0, len(arr))for i, item : range arr {if slices.Index(arr, item) i {result append(result, item)}}return result}// 方法6从右往左跳过重复// 倒序扫描遇相同则把 i 整体左移跳过这一段重复区func unique6(arr []int) []int {n : len(arr)tmp : make([]int, n)x : nfor i : n - 1; i 0; i-- {for j : i - 1; j 0; j-- {if arr[i] arr[j] {i--j i}}x--tmp[x] arr[i]}return tmp[x:]}第2类map 与 Set方法7-11策略原理Go 没有原生 Set 类型但map[T]struct{}是社区公认的 Set 惯用法——struct{} 不占内存只用键去重。把数据塞进 map去重就自然完成。map[T]bool最直观bool 值 1 字节map[T]struct{}节省内存工程首选自定义Set结构体封装 Add/Contains 接口复用性更强频次 map去重同时统计代价是元素必须可比较Go 中可比较指能用比较包含基本类型、指针、可比较的结构体等。适用场景日常项目首选。需要保序就维护一个 result 切片写入时同步推到结果尾。否是原切片逐个判重已在 map 中?哈希查询map 添加键result 追加自动忽略最后返回// 方法7map[int]bool 显式判重func unique7(arr []int) []int {seen : make(map[int]bool, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if !seen[item] {seen[item] trueresult append(result, item)}}return result}// 方法8map[int]struct{} 空值集合——Set 惯用法func unique8(arr []int) []int {seen : make(map[int]struct{}, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}// 方法9自定义 Set 结构体type IntSet struct {data map[int]struct{}}func newIntSet() *IntSet { return IntSet{data: map[int]struct{}{}} }func (s *IntSet) Add(v int) { s.data[v] struct{}{} }func (s *IntSet) Contains(v int) bool { _, ok : s.data[v]; return ok }// 方法10直接 map 转切片——写法最短但顺序随机func unique10(arr []int) []int {m : make(map[int]struct{}, len(arr))for _, item : range arr {m[item] struct{}{}}result : make([]int, 0, len(m))for k : range m {// Go 的 map 遍历顺序是随机的Go 团队故意设计的result append(result, k)}return result}// 方法11频次统计 map——去重 业务统计func unique11(arr []int) []int {count : make(map[int]int, len(arr))result : make([]int, 0, len(arr))for _, item : range arr {if count[item] 0 {result append(result, item)}count[item] // 累加频次}return result}第3类排序后去重方法12-14策略原理先sort.Ints让相同元素相邻再扫一遍删除相邻相同项。复杂度由排序决定O(n log n)。优点是不需要额外哈希结构相邻判等是最便宜的判重方式缺点是会破坏原顺序且要求元素可比较基本类型直接 OK自定义类型需要sort.Slice配合。适用场景输出本就需要排序、不在意原顺序、内存敏感。是否原切片排序相同元素相邻相邻是否相同?删后者保留结果// 方法12排序后从后往前删func unique12(arr []int) []int {sort.Ints(arr)for l : len(arr) - 1; l 0; l-- {if arr[l] arr[l-1] {arr append(arr[:l], arr[l1:]...)}}return arr}// 方法13排序后从前往后删func unique13(arr []int) []int {sort.Ints(arr)l : len(arr) - 1for i : 0; i l; i {if arr[i] arr[i1] {arr append(arr[:i], arr[i1:]...)i--l--}}return arr}// 方法14经典双指针LeetCode 26 题解法// 原地排序后在原切片上原地去重O(1) 额外空间func unique14(arr []int) []int {if len(arr) 0 {return arr}sort.Ints(arr)slow : 0for fast : 1; fast len(arr); fast {// 快指针发现新值slow 前进一步并写入if arr[fast] ! arr[slow] {slowarr[slow] arr[fast]}}return arr[:slow1]}第4类泛型与函数式方法15-17策略原理Go 1.18 引入泛型让通用工具函数成为可能Go 1.21 标准库新增slices.Compact把已排序切片相邻去重做成一行。函数式方面闭包 高阶函数也能模拟其他语言里的filter。适用场景现代 Go 工程的常态写法。可读性高、复用性好特别适合编写通用的工具库。泛型标准库高阶函数原切片选择方式unique支持任意 comparableslices.Sort Compactfilter 闭包谓词结果// 方法15泛型去重Go 1.18// comparable 约束保证 比较有效func unique15[T comparable](arr []T) []T {seen : make(map[T]struct{}, len(arr))result : make([]T, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}// 方法16标准库 slices.CompactGo 1.21// Compact 删除相邻重复元素要求已排序func unique16(arr []int) []int {slices.Sort(arr)return slices.Compact(arr)}// 方法17高阶 filter 闭包谓词func filter[T any](arr []T, pred func(T) bool) []T {result : make([]T, 0, len(arr))for _, item : range arr {if pred(item) {result append(result, item)}}return result}// 方法17高阶函数式法, 用闭包封装已存在状态模拟函数式 filterfunc unique17(arr []int) []int {seen : make(map[int]struct{}, len(arr))// 闭包捕获 seen谓词带副作用首次见到才返回 truereturn filter(arr, func(x int) bool {if _, ok : seen[x]; ok {return false}seen[x] struct{}{}return true})}第5类递归与位图方法18-20策略原理递归用自调用替代循环是函数式思维的体现主要用于教学位图用[]uint64自己实现 BitSet——每一位标记一个非负整数是否出现过对整数集合有极致的空间效率10 亿个 int 只要 128MB是大数据去重的常见选型。适用场景递归——教学、题型熟悉BitSet——大规模非负整数如用户 ID、订单号的去重统计。是否是否切片 lengthnlength 1?返回检查末尾元素是否在前面出现重复?丢弃末尾保留末尾递归 length-1// 方法18递归原地删除func unique18(arr []int, length int) []int {if length 1 {return arr}last : length - 1for i : last - 1; i 0; i-- {if arr[last] arr[i] {arr append(arr[:last], arr[last1:]...)break}}return unique18(arr, length-1)}// 方法19递归拼接返回不修改原切片纯函数式func unique19(arr []int, length int) []int {if length 1 {return []int{}}last : length - 1isRepeat : falsefor i : last - 1; i 0; i-- {if arr[last] arr[i] {isRepeat truebreak}}head : unique19(arr, length-1)if !isRepeat {head append(head, arr[last])}return head}// 方法20BitSet 位图仅适用于非负整数// 用 []uint64 自己实现位图每个 int 占一位func unique20(arr []int) []int {maxVal : 0for _, v : range arr {if v 0 {panic(BitSet 不支持负数需要先偏移)}if v maxVal {maxVal v}}bits : make([]uint64, maxVal/641)result : make([]int, 0, len(arr))for _, v : range arr {// 第 v 位为 0 表示首次出现if bits[v/64](1(v%64)) 0 {bits[v/64] | 1 (v % 64)result append(result, v)}}return result}选择指南不需要需要大量非负整数顺便要排序一般规模代码通用工程清晰按字段去重切片去重是否需要保序看数据特征保留原顺序数据规模与类型[]uint64 位图极致空间效率slices.Sort Compact标准库一行map[T]struct{} 转切片O(n) 最快侧重点unique[T comparable]泛型工具map result显式语义传入 keyFn携带业务键类别时间复杂度是否保序主要场景基础循环O(n²)是教学、面试手撕map / SetO(n)看实现日常项目首选排序后去重O(n log n)否变排序顺便要排序泛型与函数式O(n)是现代 Go 通用工具递归 / 位图视实现看实现教学 / 海量整数实际项目里怎么选绝大多数情况一个泛型函数就够// 保序、O(n)、对所有 comparable 类型有效func unique[T comparable](arr []T) []T {seen : make(map[T]struct{}, len(arr))result : make([]T, 0, len(arr))for _, item : range arr {if _, ok : seen[item]; !ok {seen[item] struct{}{}result append(result, item)}}return result}不在意顺序m : make(map[int]struct{})for _, v : range data {m[v] struct{}{}}result : make([]int, 0, len(m))for k : range m {result append(result, k)}需要排序slices.Sort(data)data slices.Compact(data) // Go 1.21海量非负整数// 用 []uint64 自己实现位图10 亿规模也只要 ~128MBbits : make([]uint64, maxVal/641)for _, v : range data {bits[v/64] | 1 (v % 64)}带业务逻辑的去重实际工作里经常遇到这样的情况遇到重复时不能简单丢弃要按某个规则做处理。比如按ID去重但要保留分数最高的那条记录去重的同时累加重复次数数值在某个区间内才参与去重这类需求 map 直接搞不定需要把判重和处理两步拆开来写。Go 里通常用泛型函数 合并函数// UniqueBy 带业务规则的去重。//// keyFn 从元素提取去重键。// onDup 遇到重复时如何合并 (旧值, 新值) - 新代表值。func UniqueBy[T any, K comparable](data []T,keyFn func(T) K,onDup func(old, new T) T,) []T {chosen : make(map[K]T)order : make([]K, 0)for _, item : range data {k : keyFn(item)if _, ok : chosen[k]; !ok {chosen[k] itemorder append(order, k)} else if onDup ! nil {chosen[k] onDup(chosen[k], item)}}result : make([]T, 0, len(order))for _, k : range order {result append(result, chosen[k])}return result}例 1按ID去重保留分数最高的type Student struct {ID intName stringScore int}students : []Student{{ID: 1, Name: 张三, Score: 90},{ID: 1, Name: 张三, Score: 95}, // 同 id分数更高{ID: 2, Name: 李四, Score: 85},}result : UniqueBy(students,func(s Student) int { return s.ID },func(old, new Student) Student {if new.Score old.Score {return new}return old},)// result: [{1 张三 95} {2 李四 85}]例 2去重同时统计频次counts : make(map[string]int)order : []string{}for _, item : range data {if _, ok : counts[item]; !ok {order append(order, item)}counts[item]}// order 是保序的去重结果counts 是频次统计例 3区间过滤——只对[0, 100]区间内的值去重区间外原样保留seen : make(map[int]struct{})result : []int{}for _, x : range data {if x 0 x 100 {if _, dup : seen[x]; dup {continue}seen[x] struct{}{}}result append(result, x)}这三个例子是同一种思路把判重与业务规则分开。判重用 map 保证 O(n)规则部分留给回调或显式分支处理。自定义对象去重comparable 约束Go 的可比较概念比 Java 的 equals 更严格——不是所有类型都能直接进 map 当键类型是否可比较备注基本类型int, string, bool 等✓直接可用指针✓比较指针地址数组[N]T✓元素可比较即可字段全部可比较的 struct✓逐字段比较接口类型✓但运行时 panic 风险slice / map / func✗不可比较含 slice 字段的 struct✗不可比较如果元素是含 slice 的 struct需要自己提取一个可比较的键type User struct {ID int64Name stringTags []string // 不可比较}// 用 ID 作为去重键seen : make(map[int64]struct{})result : []User{}for _, u : range users {if _, ok : seen[u.ID]; !ok {seen[u.ID] struct{}{}result append(result, u)}}注意Go 没有 equals/hashCode 重写机制。要按业务字段去重就要么自己定义 keyFn要么把业务等价压进一个可比较的 struct 字段。总结工程应用选择默认用泛型unique[T comparable](arr []T) []T保序、一行、O(n)标准库一步到位slices.Sortslices.Compact原地、O(n log n)、顺便排序不要顺序就直接map[T]struct{}转切片海量非负整数用[]uint64自实现位图含 slice 字段的 struct 先提取可比较键再去重