优先用 strings.contains + 预处理(统一转小写、去空格、去标点),避免正则开销;敏感词热加载用 fsnotify 监听 + 原子替换 + sync.rwmutex 保护;替换需按位置切片拼接,避免误伤;并发下不用 sync.map,改用排序切片 + mutex。

敏感词匹配用 strings.Contains 还是 regexp?
直接用 strings.Contains 最快,但只支持子串精确匹配;想支持模糊、通配、大小写不敏感或中文变体(如“和-谐”),就得上 regexp。不过正则在高频文本扫描中开销明显,尤其当敏感词库超百条时,regexp.Compile 初始化慢,Regexp.FindAllString 执行也容易成为瓶颈。实际项目里,90% 的屏蔽需求只需关键词存在即拦截,优先选 strings.Contains + 预处理(统一转小写、去空格、去标点)更稳。
- 避免对每条文本都重复调用
strings.ToLower:提前把敏感词和待检文本都转成小写再比 - 别在循环里反复
regexp.Compile,应一次性编译好存到全局map[string]*regexp.Regexp - 若需支持“*”通配符,不要手写正则替换(易出错),改用现成的
github.com/agnivade/levenshtein或自定义简单通配逻辑
如何热加载敏感词而不重启服务?
文件监听 + 原子替换是最轻量的做法。用 fsnotify 监控 words.txt 修改事件,读新文件时先解析校验格式(每行一个词,跳过空行和注释),成功后再原子更新内存中的 []string 切片——注意必须用 sync.RWMutex 保护读写,否则并发读取时可能 panic。
- 不要直接赋值
words = newWords,而要用atomic.StorePointer或双缓冲(新建切片 → 写入 → 替换指针)避免中间态不一致 - 配置文件里加校验字段,比如首行写
#version:20240521,加载时比对版本号,防止旧配置覆盖新规则 - 上线后加个 HTTP 接口
GET /api/sensitive-words返回当前加载的词数和最后更新时间,方便运维确认是否生效
替换动作该用星号 * 还是统一掩码?
用户输入 “我爱北京天安门”,屏蔽“北京”后输出 “我爱**天安门” 是常见需求,但直接用 strings.ReplaceAll 会误伤(如“背景”也被替成“**景”)。正确做法是先用 strings.Index 找到所有匹配起始位置,再按位置切片拼接,确保只替换完整词。如果要求更高(如保留原词长度、支持 Unicode 变宽字符),就得用 utf8.RuneCountInString 计算真实字符数,而不是字节长度。
- 别用
strings.ReplaceAll(text, word, "***"),它无法区分“北京”和“背景” - 敏感词含 emoji 或中文标点时,
len(word)不等于显示宽度,替换后可能错位,务必用utf8.RuneCountInString(word) - 若业务允许,建议统一返回掩码字符串如
[已屏蔽],比星号更安全,也规避了长度计算问题
为什么并发场景下 sync.Map 不适合存敏感词?
sync.Map 适合读多写少且 key 分散的场景,但敏感词列表是整体加载、整体替换的,每次更新都是全量覆盖,用 sync.RWMutex + 普通 []string 或 map[string]struct{} 更清晰、更可控。而且 sync.Map 的 Range 方法不保证顺序,而某些业务需要按词长度倒序匹配(避免“中国人民”被“中国”提前截断),普通切片可排序,sync.Map 不行。
- 别为了“线程安全”盲目套
sync.Map,它比普通 map + mutex 内存占用高、遍历慢 - 如果真要按长度排序匹配,存敏感词时就用
sort.Slice(words, func(i, j int) bool { return len(words[i]) > len(words[j]) }) - 测试时重点压测高并发下的
RWMutex.RLock()性能,多数情况下比想象中快得多,不用过早优化
真正难的不是匹配逻辑,而是怎么让运营人员能看懂配置文件、怎么让开发敢改词库而不担心线上崩、怎么在日志里留下足够线索定位误杀——这些往往比算法本身更消耗精力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











