用 map[string]struct{} 做行级去重是 go 中最常用、最稳、性能也够用的方案;别用 slices.contains 去模拟去重,它会导致 o(n²) 时间复杂度,大文件处理会卡死。

直接说结论:用 map[string]struct{} 做行级去重,是 Go 中最常用、最稳、性能也够用的方案;别用 slices.Contains 去模拟去重,它只会让程序在大文件上卡死。
为什么不能用 slices.Contains 做去重
有人看到标准库加了 slices.Contains,就以为能靠它边遍历边查重。实际完全不是一回事:
-
slices.Contains只返回bool,不改变原切片,也不生成新切片 - 如果硬套:每读一行就调用一次
slices.Contains(result, line),相当于对每个新行都做一次 O(n) 遍历——整体变成 O(n²),10 万行文件可能要跑几秒 - 它没解决“如何构建去重后结果”的问题,只是个辅助判断工具
用 map[string]struct{} 实现去重(推荐)
这是 Go 社区事实标准做法,兼顾可读性、内存效率和速度:
- 用
struct{}当 value 是因为零内存占用(unsafe.Sizeof(struct{}{}) == 0) - key 用
string直接支持按行去重;若需忽略空行或首尾空白,先用strings.TrimSpace - 保持原始顺序:遍历原切片时只往 map 里存,最后再按顺序 append 到结果切片
lines := []string{"a", "b", "a", "c"}
seen := make(map[string]struct{})
var unique []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if _, exists := seen[line]; !exists {
seen[line] = struct{}{}
unique = append(unique, line)
}
}
大文件逐行去重必须用 bufio.Scanner
别用 os.ReadFile 一次性加载整个文件——几 GB 的日志文件会直接 OOM:
-
bufio.Scanner默认缓冲区 64KB,适合流式处理;可通过sc.Buffer扩容(如处理超长行) - 每次
sc.Scan()只读一行,内存压力恒定 - 注意:
sc.Text()返回的是底层缓冲区的引用,若需长期保存,必须用string(sc.Text())拷贝一份,否则下一行会覆盖内容
去重后写回文件要注意权限与原子性
直接 os.WriteFile 覆盖原文件有风险,尤其在生产环境:
- 写入中途崩溃会导致原文件丢失;建议先写到临时文件(如
filename + ".tmp"),再os.Rename原子替换 - 保留原文件权限:用
os.Stat读取原文件Mode(),传给os.WriteFile的第三个参数 - 如果目标是追加去重结果而非覆盖,改用
os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0)
真正容易被忽略的点是:去重逻辑是否该忽略大小写、空格、BOM 或行末换行符。这些细节不提前定义清楚,看似“去重成功”的结果,上线后可能因格式差异漏掉关键重复项。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











