strings.ReplaceAll 不能直接用于敏感词过滤,因其仅支持字面量、非重叠、单次扫描替换,不处理 UTF-8 的 rune 边界,导致中文/emoji 切碎、漏匹配、误杀或 panic;必须用 []rune 处理文本和敏感词,配合 AC 自动机与 rune 索引精准掩码。

直接用 strings.ReplaceAll 做敏感词替换,90% 的线上问题都出在中文被切碎、误杀、漏匹配或 panic 上——不是函数不会调,而是没绕开 UTF-8 和树结构的坑。
为什么 strings.ReplaceAll 不能直接用于敏感词过滤
它只做字面量、非重叠、单次扫描替换:strings.ReplaceAll("王八羔子", "王八", "*") 返回 "*羔子",但“王八羔子”本身也是敏感词,却被漏掉了;更糟的是,如果敏感词含 emoji 或中文,strings.ReplaceAll 按字节操作,可能把“♥️”(U+2665 + U+FE0F,2 个 rune)错切成一半,导致匹配永远失败。
- 它不支持「重叠匹配」:无法同时捕获“王八”和“王八羔子”
- 它不感知 rune 边界:传入
"王"能匹配,但"王"在内存里占 3 字节,替换时若按字节切片会越界或错位 - 它无法定位匹配位置:你不知道“王八”出现在第几个字符,也就没法做精准掩码(比如替换成等长
"**"而不是固定"*")
必须用 []rune 处理所有文本和敏感词
Go 的 string 是 UTF-8 字节数组,for i := range s 遍历的是字节索引,不是字符位置。中文、emoji 全部乱码,前缀树建对了也查不到。
- 构建敏感词树前,统一转:
chars := []rune(word) - 扫描文本时,必须用
for _, r := range []rune(text)或for i, r := range []rune(text),绝不用text[i] - 替换掩码长度要按 rune 数算:
strings.Repeat("*", len([]rune(match))),而不是len(match)(那是字节数) - 若硬要优化纯 ASCII 场景(如日志脱敏),加校验:
if !utf8.ValidString(text) { return err }
node.Next[c] panic?90% 是没判 nil
现象是本地小文本不报错,压测时突然崩溃,堆栈指向类似 node.Next[r] = newNode 这一行——因为 node.Next 是 nil map,Go 对 nil map 写入直接 panic。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 每次访问前手动判空:
if node.Next == nil { node.Next = make(map[rune]*Node) } - 更安全的做法是封装方法:
func (n *Node) setChild(r rune, child *Node),内部统一初始化 - 别信“根节点已初始化”,树深越大,子节点的
Next越容易为 nil,尤其热更新或并发加载时 - panic 不发生在建树时,而是在匹配循环里第一次写入未初始化的分支节点时
真正能上线的替换逻辑长什么样
不是调一次 strings.ReplaceAll 就完事,而是:预建 AC 自动机 → 流式读文本 → 拿到字节偏移 → 转 rune 索引 → 算掩码长度 → 安全拼接结果。中间任何一环掉链子,就漏词、错位、panic 或 O(N²) 卡死。
- 用
github.com/BobuSumisu/ahocorasick,它返回的是字节起止位置,需用utf8.RuneCountInString(text[:start])转成 rune 索引 - 掩码长度必须等于匹配段的 rune 数:
mask := strings.Repeat("*", utf8.RuneCountInString(match)) - 大文件别全读进内存,用
bufio.NewReader分块读(建议 64KB),末尾保留 100 字节作跨块重叠缓冲,防“违\n法”类断行词漏掉 - 替换策略按业务选:审计日志用掩码,评论审核用整条拒绝,搜索高亮用
<em>王八</em>
最难的不是写完代码,是让“王八蛋”“♥️”“ABC(全角)”三种输入在压测下都不漏、不错、不崩、不慢——每一步都得盯着 rune、map、偏移、缓冲这四个点。

















