用 map[string]struct{} 做基础去重最直接:零内存开销、o(1) 查找、写法干净;但需注意无序遍历、key 必须是可比较类型、千万级数据应换布隆过滤器+二次校验、路径需归一化、并发写需加锁或分片。

用 map[string]struct{} 做基础去重最直接
绝大多数搜索结果去重场景(比如从 API 或 DB 拉回一批 id、url、title 后筛掉重复),直接用 map[string]struct{} 就够了——它零内存开销、O(1) 查找、写法干净。
- 别用
map[string]bool:虽然能用,但每个bool占 1 字节,而struct{}占 0 字节,量大时差几 MB 到几百 MB - 顺序不重要就别折腾:
for k := range m遍历是无序的,如果必须保序(比如按首次出现位置排),得额外记一个[]string作索引 - 注意 key 类型:如果去重的是结构体,得先序列化成字符串(如
json.Marshal)或用字段拼接;直接把 struct 当 map key 会报错 “invalid map key type”
千万级数据别硬扛,换 bloom filter + 二次校验
当搜索结果日增百万、缓存里要存上亿个 URL 或用户 ID 时,map 内存暴涨,这时就得上 bloom ——但它不是“替代 map”,而是“前置过滤器”。
-
bloom.Test()返回true≠ 一定存在:只是“可能有”,后续必须走 RedisSET或 DBSELECT确认,否则会漏数据 - 初始化参数不能拍脑袋:
bloom.New(10_000_000, 0.001)表示预估存 1000 万条、容忍 0.1% 误判;设小了误判率飙升,设大了浪费内存 - 它不支持删除:如果业务要“撤回某条结果”,
bloom无能为力,得靠后端存储兜底
路径类搜索结果要归一化再去重
爬虫或网关日志里的搜索路径(如 /api/user/123、/api/user/456)看着不同,语义却一样——直接字符串去重会失效,必须先做路径模板化。
- 数字要替换成占位符:
/api/order/789→/api/order/{:id},避免把所有订单路径当成不同项 - UUID、长 token、中文编码也要统一处理:正则
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}替换为{:id},%E6%88%91这类编码也统一收口 - 别在 HTTP 层做归一化:像
/user?id=123和/user/123是两条路由,语义不同,强行合并会出逻辑 bug
并发写入时 map 不是线程安全的
多个 goroutine 同时往一个 map 里 Add,不出几秒就 panic:fatal error: concurrent map writes。
- 简单方案:加
sync.RWMutex,读多写少时性能还行;但高并发写频繁时锁争用严重 - 进阶方案:用
sync.Map,适合读多写少且 key 类型固定(如string);但它不支持遍历全部 key,想取全部去重结果还得自己维护一份 slice - 终极方案:分片 map(sharded map),按 hash 把 key 分到多个子 map,各自加锁,吞吐量翻倍;但代码量和维护成本明显上升
真正麻烦的从来不是“怎么写去重”,而是“去重边界在哪”——URL 归一化规则要不要兼容大小写?时间戳精度截到秒还是毫秒?这些细节没对齐,再多的 bloom 或 sync.Map 都救不了数据语义混乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











