正则因编译开销大、回溯严重且不支持前缀命中即停,而trie树天然适合多模匹配,一次遍历即可判断任意位置是否存在敏感词,时间复杂度接近o(n),内存局部性好;但裸trie无法处理“八蛋”在“王八蛋”中的中段匹配,需升级为带失败指针的ac自动机实现无回退跳转。

为什么不用正则而选 Trie 树做敏感词匹配
正则在敏感词数量少、模式简单时够用,但词库一过千,regexp.Compile 编译开销大,运行时回溯严重,且无法支持“前缀命中即停”这类高效脱敏逻辑。Trie(前缀树)天然适合多模匹配:一次遍历就能判断从当前位置起是否存在任意敏感词,时间复杂度接近 O(n)(n 为文本长度),且内存局部性好,缓存友好。
注意:别直接手写 Trie 节点嵌套结构——Go 中指针跳转频繁易触发 GC 压力;推荐用数组索引代替指针,或使用 sync.Pool 复用节点。
如何构建带失败指针的 AC 自动机(而非裸 Trie)
纯 Trie 只能匹配“从头开始”的词,比如文本 "王八蛋" 中含 "八蛋",裸 Trie 会漏掉——除非对每个位置都重新查一遍,性能爆炸。AC 自动机通过失败指针(fail pointer)实现“自动跳转”,让匹配过程不回退。
实操建议:
- 用
map[rune]*node实现子节点映射,比固定 65536 大小数组更省内存(中文主要用 Unicode 中的 CJK 区间) - 构建 fail 指针时,BFS 遍历比 DFS 更稳;根节点所有子节点的
fail指向 root,其余节点按层推导 - 每个节点存
endWord string(完整敏感词)和isEnd bool,避免匹配中途无法还原原词 - 若需支持“模糊匹配”(如“王**蛋”),不要在 AC 构建阶段处理,而是在匹配后对命中的
endWord单独脱敏
脱敏逻辑必须与匹配解耦,且支持多种策略
匹配只是定位,脱敏是独立动作。把替换逻辑硬编码进 Trie 遍历里,会导致策略变更(如星号替换、拼音替换、URL 跳转)必须改核心匹配代码,极难维护。
推荐做法:
- 匹配阶段只返回
[]struct{Start, End int; Word string}切片,不含任何替换行为 - 后续用统一函数
maskText(text string, matches []Match) string处理,传入策略函数如func(word string) string { return strings.Repeat("*", len(word)) } - 注意 UTF-8 字符边界:
len("王") == 3,但脱敏要按 rune 数,不是 byte 数;用utf8.RuneCountInString和strings.Cut或utf8.DecodeRuneInString安全切分
并发安全与热更新怎么落地
生产环境词库会动态增删,但 AC 自动机重建代价高,不能每次更新都 reload 整棵树。同时,高频文本脱敏请求要求匹配过程无锁。
可行方案:
- 用
atomic.Value存储当前生效的*ACAutomaton实例,更新时构造新实例再原子替换,读路径完全无锁 - 词库变更走独立管理服务(如监听 etcd key 变更),避免业务代码耦合配置中心 SDK
- 慎用
sync.RWMutex包裹整棵树——读多写少场景下,RWMutex 的写饥饿问题会导致更新延迟升高 - 如果词库超 10 万条,考虑分片:按首字符哈希到多个子 AC 实例,查询时并行跑再合并结果,但要注意跨分片的敏感词(如“中华人民”被拆成“中华”+“人民”)需额外兜底扫描
真正难的不是建树,是让树在不停机前提下响应词库变化,且不因脱敏逻辑侵入匹配内核——这两点没做好,系统上线后第一周就会出诡异漏匹配或 panic。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











