用 map[rune]Node 而不用 map[byte]Node,因为中文、emoji等多字节字符用 byte 会拆分成乱码单元导致匹配失效,如“王”在 UTF-8 中占 3 字节但仅对应 1 个 rune。

为什么用 map[rune]*Node 而不用 map[byte]*Node
因为中文、emoji、全角符号等都不是单字节,byte 会把一个汉字拆成多个乱码单元,导致匹配完全失效。比如 "王" 在 UTF-8 下是 3 个 byte,但作为字符(rune)只有 1 个。
实操建议:
• 所有字符串遍历必须用 for _, r := range word,不是 for i := range word
• 构建树和匹配时,输入文本、敏感词都先转为 []rune 或直接 range,别碰 []byte
• 如果硬要用 byte(比如只处理 ASCII 日志),得提前做字符集校验,否则线上一跑中文就漏词
BuildDICT 函数里 node.isEnd = true 放错位置会怎样
常见错误是把 isEnd = true 写在循环内部,导致“王八蛋”中“王”“王八”“王八蛋”全被标为敏感词结尾——结果“王”单独出现就被拦截,误杀率飙升。
正确做法只在完整词路径末端设 isEnd:
• 循环完所有 rune 后再赋值
• 别在每轮子节点创建时就设 isEnd
• 如果要支持“前缀敏感”(如“王八”和“王八蛋”都算),得额外加字段如 isPrefix,不能复用 isEnd
匹配时遇到“王八羔子”却只命中“王八”,为什么没继续往后找
这是 DFA 实现最常踩的坑:只做「单次最长匹配」或「首次匹配即返回」,而没实现「重叠/多模式匹配」。
典型表现:
• 输入“王八羔子坏”,只返回“王八”,漏掉“王八羔子”
• 输入“我讨厌王八,也讨厌王八羔子”,只报第一个
解决关键点:
• 匹配不能一碰到 isEnd 就 break,得记录当前位置,然后从下一个字符重新进树(即「回溯起点+1」)
• 或者用双指针:start 固定起点,end 推进找最长匹配,找到后 start++ 继续
• 简单场景可用 strings.ReplaceAllFunc 配合 contains 判断,但性能差;高并发务必手写滑动匹配逻辑
Next 字段初始化不检查 nil 会导致 panic
Go 的 map 访问 nil map 会直接 panic,而很多人写 node.Next[c] 前没确认 node.Next != nil。
现象:
• 本地小数据测不出,压测时突然 panic: assignment to entry in nil map
• 错误堆栈指向 AddChild 或匹配循环里的 node.children[char]
安全写法:
• 所有对 Next 的读写前,加 if node.Next == nil { node.Next = make(map[rune]*Node) }
• 更推荐封装方法:func (n *Node) getChild(r rune) *Node 和 func (n *Node) setChild(r rune, child *Node),内部统一判空
• 根节点初始化必须显式 children: make(map[rune]*Node),不能依赖结构体字段默认值
立即学习“go语言免费学习笔记(深入)”;
DFA 看似简单,但中文分词边界、重叠词处理、nil map 访问这三处,90% 的线上问题都出在这儿。别信“跑通 demo 就能上线”。










