中文敏感词匹配不能用 strings.Contains 或正则,因其按 byte 操作不识别 rune,易因 UTF-8 编码、零宽字符等导致偏移错位;Trie 构建时 isEnd 必须在循环外设置,否则引发误杀;需校验 UTF-8 有效性、返回 rune 索引、细粒度并发控制,并确保所有预处理在 rune 层进行。

为什么不能用 strings.Contains 或正则做中文敏感词匹配
因为它们只按字节操作,不识别 rune。比如 "王八蛋" 是 3 个字符,但 []byte 长度是 9;插入一个零宽空格 \u200b 或 emoji 后,strings.Index 返回的 byte 偏移会错位,替换或标注时直接切歪。更糟的是,regexp.MustCompile(".*王八.*") 在 UTF-8 下可能跨 rune 边界失效,且编译和回溯开销大,线上扛不住高并发。
Trie 插入时 isEnd = true 必须放在循环外
这是线上误杀率飙升的最常见原因。错误写法在每层 for 循环里都设 node.isEnd = true,导致 “王八蛋” 插入后,“王”“王八”“王八蛋” 全被标记为终点——用户输 “王者荣耀”,立刻触发拦截。
正确做法:
- 遍历完所有
rune后,只在最终节点设node.isEnd = true - 若需支持前缀敏感(如 “王八” 和 “王八羔子” 都算违规),额外加字段
isPrefix bool,别复用isEnd - 插入前必须校验:
if !utf8.ValidString(word) { return errors.New("invalid utf8") },否则非法编码会破坏树结构
匹配阶段必须返回 rune 索引,而非 byte 偏移
只返回布尔值(如 trie.Contains(text))或第一个命中位置,等于放弃合规审查能力。真实需求是知道 “王八羔子” 在 Start=12, End=15(rune 索引),同时 “王八” 在 Start=12, End=13,且都要上报。
立即学习“go语言免费学习笔记(深入)”;
推荐双指针扫描逻辑:
-
start固定,end向右推进;每次从start重新进树 - 遇到
node.isEnd == true就记录当前start和当前end(rune 位置) - 遇到
node.children == nil时不能 panic,要先判空:if node.children == nil { break } - 返回结构至少含:
Keyword string、Start int、End int,避免后续脱敏时 byte/char 混淆
并发读写 Trie 时锁粒度不能锁整棵树
敏感词库更新极少,但匹配请求每秒数千次。整棵树加一把 sync.RWMutex,等于把全部请求串行化——吞吐直接崩掉。
可行方案:
- 读多写少场景下,用
sync.RWMutex锁根节点即可,但仅限词库全量热替换(非增量) - 真正高并发时,应分离锁粒度:比如按首字符哈希分片,每片独立锁;或用
atomic.Value替换整棵*TrieNode根,写时重建树、读时原子切换 - 别碰多级布隆过滤器——它根本不能做初筛,漏判率太高,合规过不了关
[]rune 层做;一旦在 []byte 层操作,重建 string 时长度变化会导致索引彻底错乱。


















