生产环境使用AC自动机需改三处:词典预处理(过滤空串、超长串、去重、排序)、编码统一(源头转UTF-8)、状态隔离(禁止State复用);否则90%的“不生效”或“卡顿”由此引发。
直接用 github.com/burntsushi/aho-corasick 就能跑通,但生产环境必须改三处:词典预处理、编码统一、状态隔离——否则 90% 的“不生效”或“卡顿”都源于这三点。
Build() 卡住或 panic 的真实原因
不是算法慢,是词典没清洗。AC 自动机对输入极其敏感:""、"\x00"、纯空白字符串(strings.TrimSpace()后为空)、超长串(如 >256 字节)都会导致 Build() 阶段 panic 或 fail 指针链异常。
- 必须过滤掉所有
""、"\x00"和strings.TrimSpace(w) == ""的项 - 建议限制单模式最大长度,例如
if len(w) > 256 { continue } - 必须去重:大小写不同但语义相同的词(如
"password"和"Password")应视为同一规则,用map[string]struct{}去重,别信业务侧“已唯一” - 排序推荐按长度升序 + 字典序,能让前缀复用更充分(
"user"和"username"共享前 4 个节点)
GBK / Big5 文本传给 FindAllStringIndex() 必然错位
Go 字符串默认 UTF-8,但日志、旧系统导出文本常为 GBK、Big5 或无 BOM 的 ANSI。直接传入会导致 FindAllStringIndex() 错位切分,匹配位置偏移、漏词,甚至返回负索引或 panic。
- 真实场景中,90% 的“AC 不生效”问题根源在此,而非自动机构建逻辑
- 别用
golang.org/x/text/encoding在匹配前转码——它引入额外分配和同步开销,高并发下易成瓶颈 - 正确做法:在读取源头就解码为 UTF-8
[]byte;或改用基于byte的双数组 Trie 实现(如github.com/grepner/go-ahocorasick优化分支),绕过rune层抽象
10 万+ 敏感词时 map[rune]*Node 内存爆炸
多数开源 AC 库(如 github.com/BobuSumisu/aho-corasick)默认用 map[rune]*Node 存子节点,内存占用随模式数非线性增长。10 万模式下构建耗时常超 2s,GC 压力陡增。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 生产环境应优先选双数组 Trie(Double Array Trie)实现,比如
github.com/anknown/ahocorasick或自研版本 - 双数组结构将节点压缩为两个整型切片(
base[]和check[]),内存占用降为原来的 1/10,构建速度提升约 10 倍 - 注意:双数组不天然支持 Unicode(
rune),中文支持需额外做字符映射(如 GBK 字节 →uint16ID)
并发匹配时 State 复用等于自毁
sync.Pool 对 AC 自动机的 State 复用意义不大——自动机结构只读,但匹配过程中的当前节点指针、路径深度、已匹配长度等状态必须每个查询独占,否则并发匹配会相互覆盖。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
pool.Get().(*Matcher).Search(text)且不重置内部游标 - 正确做法:每次调用
Search()都从root开始;或显式传入干净的上下文结构体(含current *Node字段) - 若追求极致性能,可预分配一批游标对象池,但池内对象必须在
Put()前清零所有字段,不能残留上一次的状态
最易被忽略的是:词典清洗和编码转换必须前置到数据摄入环节,而不是在 Build() 或 Search() 里补救——这两步一旦漏掉,再快的双数组 Trie 也救不回线上漏匹配或超时熔断。


















