用map保序去重最稳妥:先遍历a再b,用map[string]struct{}记录已见元素,仅首次出现时追加;结构体需字段可比较或手动构造键;指针要解引用;unicode需nfc归一化。

slice 去重没有内置函数,直接用 map 是最稳的路径——但「拼接后去重」这个动作,容易在合并阶段就埋下坑:顺序错乱、结构体 panic、指针误判、Unicode 不等价。
用 map 合并多个 slice 并保序去重
这是最常见也最容易写错的场景。比如从两个 API 拿到 []string,想合起来去重且保持首次出现顺序。
- 别先
append(a, b...)再统一去重——这样b里的重复项可能覆盖a中同值元素的原始位置,但逻辑上你想要的是“全局首次出现” - 正确做法是按来源顺序遍历:先扫
a,再扫b,只在!seen[v]时才追加 -
seen必须是map[string]struct{}(不是bool),省内存且语义清晰 - 别在循环里反复
make(map[string]struct{})——高频调用建议复用或走sync.Pool
示例:
func MergeUnique(a, b []string) []string {
seen := make(map[string]struct{})
var result []string
for _, s := range a {
if _, exists := seen[s]; !exists {
seen[s] = struct{}{}
result = append(result, s)
}
}
for _, s := range b {
if _, exists := seen[s]; !exists {
seen[s] = struct{}{}
result = append(result, s)
}
}
return result
}
拼接含结构体的 slice 时去重失败?检查字段可比较性
Go 编译器会直接报错:invalid map key type MyStruct——只要结构体里有 []int、map[string]int 或 func() 字段,就不能当 map 键。
- 别试图用
json.Marshal转字符串再当 key:浮点数精度、nilslice 和空 map 序列化结果不一致,导致语义重复没被识别 - 更轻量的做法是手动构造唯一键:只拼关键字段,如
s.ID + "|" + s.Status,但得确保这些字段本身不含分隔符或需转义 - 如果必须用完整结构体内容比对,且数据量小,可用
reflect.DeepEqual配合双重循环,但别用在 hot path - 指针类型如
[]*User直接塞进map[*User]struct{}比的是地址,不是内容;要内容去重就得解引用后比字段
slices.Compact 不能直接用于拼接后去重
有人看到 Go 1.21+ 的 slices.Sort + slices.Compact 就想套用,但这条路只适用于「单个已排序 slice」,和「拼接后去重」是两回事。
-
slices.Compact只删相邻重复项,前提是 slice 已升序/降序排列;拼接后的append(a, b...)是乱序的,直接 Compact 几乎无效 - 若强行先
sort.Slice再slices.Compact,顺序完全丢失,且结构体无法直接排序(除非实现Less) - 该组合适合:你本就不care顺序,且数据是纯数值或字符串、规模大、内存敏感——此时排序+原地 compact 确实比 map 省内存
- 注意
slices.Compact返回的是新长度,要用s[:n]截取,不是返回新 slice
Unicode 字符串拼接去重时的隐性不等价
"café" 和 "cafe\u0301" 在 Go 里是两个不同字符串,map 不会合并它们——即使人眼看来完全一样。
- 这不是 bug,是 UTF-8 编码层面的客观事实
- 如果业务要求语义去重(比如用户昵称、URL 路径),必须先做 Unicode 归一化(如 NFC 标准),再进
map - 标准库不提供归一化,得引入
golang.org/x/text/unicode/norm,调用norm.NFC.String(s) - 别在每次循环里都调用归一化——提前处理好输入源,或缓存归一化结果
真正麻烦的从来不是“怎么写”,而是“凭什么认为这两个值相等”。去重逻辑一旦脱离可比较性定义,就只能靠业务规则兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











