必须用[]rune的trie树+显式重叠匹配逻辑,因strings.contains和正则仅按字节匹配,无法正确处理中文、emoji等utf-8多字节字符,易漏判错位;isend=true须在rune遍历完成后设置,避免前缀误判;匹配需支持重叠定位并返回rune索引。

别用 strings.ReplaceAll 或正则硬扫,也别碰多级布隆过滤器——它根本不能做敏感词初筛。真要高效又合规,必须用基于 []rune 的 Trie 树 + 显式重叠匹配逻辑。
为什么 strings.Contains 和正则在中文场景下必然失效
它们只认字节,不认字符。输入 “王八蛋”,[]byte("王八蛋") 长度是 9,但实际只有 3 个 rune;直接拿 strings.Contains(text, "王八") 可能命中,但一旦文本含 emoji(如 “?王八”)或用户插入零宽空格(\u200b),就彻底漏判。
- 正则
.*王八.*在 UTF-8 下无法跨 rune 边界可靠匹配,且编译开销大、回溯风险高 -
strings.Index类函数返回的是 byte 偏移,后续替换或标注位置时会错位 - 所有预处理(去空格、转小写)必须在
[]rune层做,而非[]byte—— 否则 “草→艹” 替换后长度变化,string重建会出错
Trie 插入时 isEnd = true 必须放在循环外
这是线上误杀率飙升的根源。错误写法把 node.isEnd = true 写进 for 循环里,导致 “王八蛋” 插入后,“王”“王八”“王八蛋” 全被标为终点节点。用户输 “王者荣耀”,立刻触发拦截。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确顺序:遍历完所有
rune后,在最终节点设node.isEnd = true - 如果需支持前缀敏感(如 “王八” 和 “王八羔子” 都算违规),额外加字段如
isPrefix bool,别复用isEnd - 插入前必须校验:
if !utf8.ValidString(word) { return errors.New("invalid utf8") },否则非法编码会导致树结构损坏
匹配阶段必须支持重叠与定位,不能一命就停
只返回第一个匹配项(比如用 trie.Contains(text) 布尔判断)等于放弃合规审查能力。真实需求是:知道 “王八羔子” 在位置 12–15,同时 “王八” 在 12–13,且都要上报。
- 推荐双指针扫描:
start固定,end向右推进;每次从start重新进树,记录所有isEnd == true的终点位置 - 匹配过程中,
node.children为空时不能 panic,必须先判nil:if node.children == nil { break } - 返回结构至少含:
Keyword string、Start int(rune索引)、End int,避免后续脱敏或日志时 byte/char 混淆
并发读写 Trie 时锁粒度必须分离
整棵树加一把 sync.RWMutex 是吞吐杀手。敏感词库更新极少,但匹配请求每秒数千次,锁住根节点等于串行化全部请求。
- 词库加载/热更新:写操作,用
mutex.Lock()替换整个root指针(原子替换) - 文本匹配:纯读,只要不修改
childrenmap 或isEnd字段,for _, r := range []rune(text)天然线程安全 - 若真需边写边读(如动态增删),改用不可变 Trie + CAS 根节点,或用
sync.Map存储各层节点,但代价是内存翻倍、GC 压力上升
最易被忽略的点:所有路径都依赖 []rune 统一视角,从建树、插入、扫描到结果定位,任意一环退回到 []byte 就会引发错位——不是性能问题,是功能性崩溃。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










