应使用双指针+trie树而非滑动窗口做敏感词过滤,因go中string为utf-8字节数组,需统一用rune遍历、判空children、支持重叠匹配,大规模场景宜用ac自动机。

别用滑动窗口做敏感词过滤——它在 Go 里根本跑不通中文、emoji 和变长词,硬上只会漏词、panic、性能更差。真要简单又可靠,就用双指针 + Trie 树,50 行内能跑通生产级逻辑。
为什么 for i := range text 会导致中文匹配全失效
Go 的 string 是 UTF-8 字节数组,for i := range text 遍历的是字节索引,不是字符位置。“王”占 3 字节,text[0] 取出来是乱码 byte,根本进不了 Trie 节点分支。
- 所有字符串遍历必须写成
for _, r := range text,或提前转chars := []rune(text),后续操作基于chars[i] - 构建敏感词树、扫描输入文本、跳转子节点——三处全得统一用
rune,一处用byte就崩 - 测试时至少加一条用例:
"王八蛋♥️",否则上线后中文静默漏词,日志里连错误都没有
node.children[r] panic 的真实原因和安全写法
现象是压测时突然报 panic: assignment to entry in nil map,堆栈指向 node.children[r] = child 这一行。本质是某个中间节点的 children 还是 nil,却直接赋值。
- 每次访问
node.children前必须判空:if node.children == nil { node.children = make(map[rune]*Node) } - 别在
Add函数里只初始化根节点,深层节点(比如“王→八→蛋”的“八”节点)也得各自初始化自己的children - 更稳妥的做法是封装方法:
func (n *Node) getChild(r rune) *Node和func (n *Node) setChild(r rune, child *Node),内部统一处理 nil map
匹配“王八”和“王八羔子”必须支持重叠,不能一碰到 isEnd 就退出
如果匹配逻辑写成“遇到 node.isEnd == true 就返回”,那输入 "王八羔子" 只会命中 "王八",漏掉更长的 "王八羔子" ——因为没继续推进 end 指针。
- 正确做法是:每次
node.isEnd == true时记录[start, end],然后继续让end++,查下一个字符是否还能往下走 - 确保
isEnd = true只在完整敏感词末尾节点设置,绝不在循环体内提前赋值;否则“王八蛋”会让“王”“王八”都标为结尾,导致误杀 - 若需同时支持前缀敏感(如“王八”和“王八羔子”都算),得额外加字段如
isPrefix bool,不能复用isEnd
用 github.com/bobusumisu/aho-corasick 替代手写 Trie 的实操条件
AC 自动机不是“更高级的玩具”,而是当词库超 500 条、QPS 超 1k 时的刚需。手写 Trie 在小规模下够用,但一到真实场景,AC 的 O(n+k) 就碾压 O(n×m)。
- 初始化必须复用实例:
ac := aho.New();ac.Build(patterns),别每次请求都重建 - 词库变更时要重建 AC 实例,并用
sync.RWMutex保护读写,避免热更新期间匹配错乱 - 匹配返回
[]ac.Match,含Start/End/Keyword,脱敏时严格按坐标替换,别用strings.ReplaceAllString——它不认重叠、不保位置、大小写混排就失效
最易被忽略的点:哪怕你代码里写了“滑动”,只要没统一用 []rune、没对每个中间节点判 children 是否为 nil、没在 isEnd 设置上卡死末端,那这个“简单引擎”在线上就是个定时漏词器。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











