必须用双数组Trie实现AC自动机,避免map[rune]*Node导致构建卡顿、内存暴涨;词典需清洗空字符串、控制长度、去重大小写、排序以提升前缀复用。

直接上生产必须用双数组 Trie 实现,别碰默认 map[rune]*Node 版本——10 万词典下构建卡 2s+、内存暴涨、GC 频繁,不是配置问题,是数据结构瓶颈。
为什么 Build() 卡住或 panic
AC 自动机对词典质量极其敏感,Build 阶段崩溃或耗时异常,90% 源于未清洗输入:
-
""、"\x00"、strings.TrimSpace(w) == ""的条目会直接触发 panic - 单模式超长(如 >256 字节)会导致节点爆炸,尤其在中文场景下易出现“身份证号XXXXXXX…”这类脏数据
- 大小写不一致但语义重复的词(
"password"和"Password")会让 fail 指针链冗余甚至断裂 - 未排序:按长度升序 + 字典序排列能显著提升前缀复用率,
"user"和"username"才会共享前 4 个节点
清洗示例(必须在 ac.Build() 前执行):
words := []string{"密码", "Password", " ", "\x00", "username", "user"}
cleaned := make([]string, 0, len(words))
seen := map[string]struct{}{}
for _, w := range words {
w = strings.TrimSpace(w)
if w == "" || len(w) > 256 {
continue
}
if _, ok := seen[strings.ToLower(w)]; !ok {
seen[strings.ToLower(w)] = struct{}{}
cleaned = append(cleaned, w)
}
}
sort.Slice(cleaned, func(i, j int) bool {
if len(cleaned[i]) != len(cleaned[j]) {
return len(cleaned[i]) < len(cleaned[j])
}
return strings.ToLower(cleaned[i]) < strings.ToLower(cleaned[j])
})
GBK/Big5 文本匹配失效的根本原因
Go 字符串是 UTF-8,但日志、数据库导出、旧系统接口常返回 GBK。直接传 []byte 或 string 给 FindAllString() 会导致 rune 切分错位——匹配位置偏移、漏词、甚至返回负索引。
立即学习“go语言免费学习笔记(深入)”;
错误做法:golang.org/x/text/encoding 在每次匹配前转码,高并发下成为性能瓶颈且引入额外内存分配。
正确路径只有两条:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 在数据源头解码为 UTF-8
[]byte(如读文件时用golang.org/x/text/encoding/simplifiedchinese.GB18030.NewDecoder().Bytes()) - 改用基于
byte的双数组 Trie 实现(如github.com/grepner/go-ahocorasick的优化分支),绕过 rune 抽象层
切记:FindAllStringIndex() 不是万能入口,它只适配 UTF-8;混编码文本必须先归一化。
并发匹配时 State 复用的陷阱
自动机结构本身只读,但匹配过程中的游标状态(当前节点指针、已匹配长度、路径深度)必须 per-query 独立。复用未清零的 *Matcher 实例会导致结果污染。
典型错误写法:
var matcher *ahocorasick.Matcher
func handle(text string) {
// 错误:复用同一实例,无重置逻辑
matches := matcher.FindAllString(text)
}
安全做法(二选一):
- 每次调用都新建干净实例(轻量,推荐):
ac := ahocorasick.New(...); ac.Build(dict); ac.FindAllString(text) - 若追求极致性能,用
sync.Pool管理游标对象,但Put()前必须显式清零所有字段(current = root、matchedLen = 0等),不能依赖 GC
注意:github.com/BobuSumisu/ahocorasick 默认不支持热更新,词典变更必须重建整个自动机——这点在规则频繁下发的敏感词系统中常被忽略。
真正难的不是“怎么写 AC”,而是让 10 万词典在 200ms 内建完、每秒扛住 5k 并发查询、且不因一个 GBK 日志就崩掉整条 pipeline。双数组结构、源头编码归一、词典预清洗,三者缺一不可。


















