AC自动机比strings.Contains更高效可靠,支持嵌套词精确匹配、O(n+m)时间复杂度,推荐使用github.com/BobuSumisu/ahocorasick库,需预处理词库、倒序替换防偏移、归一化干扰符,并复用自动机构建实例。

敏感词匹配用 AC 自动机比简单 strings.Contains 更靠谱
直接遍历敏感词列表 + strings.Contains 在词库超过几百个时就会明显变慢,且无法处理“苹果”和“苹果手机”这类嵌套词的精确命中。AC 自动机(Aho-Corasick)能一次性扫描文本,时间复杂度接近 O(n + m),其中 n 是文本长度、m 是匹配结果数,是工业级脱敏的常见选择。
推荐用现成库 github.com/BobuSumisu/ahocorasick(轻量、无依赖、支持 Unicode),避免自己实现有 bug 的状态机。
- 初始化一次自动机构建词典,后续重复使用;不要每次调用都重建
- 敏感词需预处理:去重、trim 空格、统一转小写(若不区分大小写)
- 注意词库中含正则元字符(如
.、*)不会被自动转义——该库只做字面匹配,无需担心注入 - 若需支持模糊匹配(如“苹*果”),得额外加规则层,AC 本身不提供
脱敏替换逻辑必须保留原始位置和长度
不能用 strings.ReplaceAll 简单粗暴全替,否则“苹果手机”里“苹果”被替成 ** 后变成 **手机,但“苹果”本身长度是 2,** 长度也是 2,看似没问题——可一旦替换成 [屏蔽](长度 4),就撑开原文,导致后续匹配偏移错乱。
正确做法是收集所有匹配区间 [start, end),按起始位置倒序排序,再从后往前替换,避免索引漂移。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 用
ac.FindAllStringIndex(text, true)获取所有重叠匹配(第二个参数设为true) - 匹配结果是
[][2]int,每个元素形如[2]int{start, end} - 务必倒序处理:先改末尾,再改前面,否则
text = text[:i] + "[屏蔽]" + text[j:]会因前面修改导致后面i、j失效
中文分词不是必须,但需防“黏连词”漏匹配
AC 自动机本身是字节/符文级别匹配,对中文天然友好。但用户输入常带空格、标点、全角半角混用,比如“苹果 手机”(中间是全角空格)或“苹\u200b果”(零宽空格),会导致匹配失败。
与其上重型分词器(如 gojieba),不如做轻量预处理:
- 将常见干扰符统一归一化:
strings.ReplaceAll(text, " ", " ")(全角空格→半角) - 过滤掉零宽字符:
strings.Map(func(r rune) rune { if unicode.IsControl(r) && !unicode.IsSpace(r) { return -1 }; return r }, text) - 不建议直接删所有标点——可能误伤“微信支付”里的“支#付”这种带符号的敏感变体
- 若业务明确要求“词语级别”匹配(如“苹果”不匹配“苹果派”),才需分词+关键词提取,此时 AC 退居次位,改用分词结果查敏感词集合
性能关键点:复用自动机实例和字符串构建
高频调用场景下,strings.Builder 比 += 拼接快 3–5 倍,而 AC 自动机结构体本身可安全并发读(无内部状态变更),但写操作(AddWord)必须串行。
- 全局初始化一个
*ahocorasick.AhoCorasick实例,用sync.Once加载词库 - 每次脱敏新建
strings.Builder,最后用b.String()输出;避免 []byte 手动拼接出错 - 不要在匹配循环里反复调用
len(text)或text[i:j]——Go 中切片操作本身廉价,但频繁取子串会分配新底层数组 - 如果敏感词极少(strings.Contains 更快,别为架构感硬套 AC

















