gin中间件直接用strings.replaceall或contains做敏感词过滤必然失败,因性能随词库线性恶化、无法处理重叠匹配、多字节字符切片错乱;必须用支持unicode的ahocorasick库,统一转[]rune处理,匹配结果按起始位置降序替换,并用sync.rwmutex或atomic保护热更新。

直接用 Gin 的中间件做敏感词替换,不接 AC 自动机或正确处理 rune,上线后中文漏词、emoji 失效、高并发卡顿是必然结果——不是“可能”,是“一定”。
为什么 Gin 中间件里不能用 strings.ReplaceAll 或 strings.Contains
这类操作在请求体上逐个关键词扫,性能随词库线性恶化。100 个词时延迟尚可,1000 个词(常见生产规模)单次过滤就超 5ms;遇到“王八蛋”“王八羔子”这种嵌套词,strings.Contains 根本无法识别重叠匹配,只返回第一个短词,漏掉真正高危项。
更致命的是:Gin 的 c.Request.Body 是 io.ReadCloser,读一次就耗尽,不做缓冲直接 io.ReadAll 可能导致后续绑定失败;而所有基于 []byte 的暴力扫描都会把“♥️”“?”这类多字节字符切碎,匹配永远失败。
- 别写
for _, w := range words { text = strings.ReplaceAll(text, w, "*") } - 别在中间件里对原始
body直接bytes.Contains - 测试必须包含
"王八蛋"、"♥️你"、"ABC"(全角)三类输入,缺一不可
如何在 Gin 中间件里安全接入 github.com/BobuSumisu/ahocorasick
这个库是目前 Go 生态唯一支持 Unicode、增量构建、且不 panic 的 AC 自动机实现。它要求所有输入统一为 []rune,不能传 []byte 或 string 原样调用。
实操要点:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 初始化时用
ahocorasick.NewTrie(),再调trie.AddWord()插入每个[]rune(word),最后trie.Build() - 匹配前先转文本:
rs := []rune(text),再调trie.FindAllStringIndex(string(rs))——注意:该方法内部会再转回string,但前提是输入已确保 UTF-8 合法;建议加if !utf8.ValidString(text) { return text }防御 - 返回的
[][]int是字节偏移,不是rune索引,替换时必须用bytes.ReplaceAll或strings.Builder拼接,不能直接text[i:j]切片(UTF-8 下字节偏移 ≠ 字符偏移) - 全局
*ahocorasick.Trie必须用sync.RWMutex保护写操作,读匹配无需锁
替换逻辑怎么写才不漏、不炸、不乱码
直接按 FindAllStringIndex 返回的区间从左到右替换,会因重叠导致后续偏移错乱。例如 “ababa” 匹配 “abab” 和 “baba”,第二次替换时原位置已变。
正确做法是:收集全部匹配区间后,按起始位置降序排列,再从右往左替换。这样每次替换都不影响左侧未处理区间的原始字节偏移。
- 用
strings.Builder构造结果:从末尾开始,每次b.WriteString(text[prevEnd:])+b.WriteString(replaceStr) - 替换符长度必须与原词字节数一致:
replaceStr := strings.Repeat("*", utf8.RuneCountInString(word)),否则 “王八蛋” 被替成三个*,前端显示异常 - 若需保留原始格式(如富文本中只替换文本节点),不要在中间件里全量解析 HTML,改用专用清洗库如
mvdan.cc/xurls提前提取纯文本段再过滤
热更新敏感词库时 Gin 接口怎么设计才不崩
文件监听(fsnotify)在容器或 NFS 下极不可靠;轮询 os.Stat 浪费 CPU;而直接在 HTTP handler 里调 trie.Build() 会导致请求阻塞甚至超时。
推荐方案:暴露一个带鉴权的 reload 接口,内部原子替换指针。
- 接口路径如
POST /admin/filter/reload,校验 headerX-Admin-Token - 加载新词库后,构建新
trie,成功则atomic.StorePointer(&globalTrie, unsafe.Pointer(newTrie)),失败则保留旧 trie 并 log 错误 - 匹配函数中用
trie := (*ahocorasick.Trie)(atomic.LoadPointer(&globalTrie))读取,零拷贝、无锁、安全 - 绝不允许在 reload 过程中修改正在使用的 trie 节点字段——AC 树结构不可变,局部 patch 必然漏词
最易被忽略的一点:所有敏感词插入前,必须过一遍 regexp.QuoteMeta。如果词库里有 "a.b" 这种含正则元字符的词,不转义会破坏 AC 机内部状态,导致整棵树匹配失效,且无任何错误提示。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










