Gin 中 c.Request.Body 是一次性可读的 io.ReadCloser,首次读取后即耗尽;若需在中间件(如敏感词过滤)和后续处理器(如 c.ShouldBindJSON)中多次使用,必须手动捕获字节并用 io.NopCloser(bytes.NewBuffer(data)) 重置 Body。

为什么不能直接用 strings.ReplaceAll 做敏感词过滤
它会把“草”替成“*”,但“caoo”“艹”“口口口”完全漏过;更麻烦的是,“手枪模型”里的“枪”、“呛咳”里的“呛”也会被误杀。这不是精度问题,是底层逻辑错——strings.ReplaceAll 是单模式、无上下文、不支持 Unicode 字符边界对齐的暴力替换。
aho-corasick.Trie 初始化必须在服务启动时完成
每次请求都调 NewTrie() + Build(),CPU 和内存都会飙高,100 QPS 就可能卡住。真实场景里,词库变更频率低(每天至多 1–2 次),但查询高频(每秒数百次),必须预加载。
- 词库读取后统一转
[]rune,再逐条插入,避免 UTF-8 字节切分错位 - 插入前用
strings.TrimSpace()清空首尾空白,过滤空串和"\x00" - 构建完立刻用
trie.FindAllStringIndex("测试文本", true)验证是否返回正确[][]int坐标,别等上线才发现匹配失效 - 热更新时用
sync.RWMutex包裹 trie 实例,写时加Lock(),读时用RUnlock()
如何组合过滤:先归一化,再匹配,最后按坐标脱敏
用户输入 “h e x i a n” 或 “和-谐”,aho-corasick 默认不识别。必须在匹配前做轻量归一化,但不能破坏原始位置信息——否则脱敏时星号会偏移。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 归一化只作用于匹配过程:把输入文本复制一份,转小写、去空格、替换常见绕过符号(如
"-" → ""、" " → ""、"→" → ""),再喂给trie.FindAllStringIndex() - 原始文本不动,拿到匹配坐标后,直接在原字符串上用
utf8.DecodeRuneInString定位 rune 起止,确保 “王八蛋” 不被切成两半 - 脱敏不用
strings.Replace,而是用[]rune切片拼接:runeSlice[i] = '*',再string(runeSlice)转回 - 若需保留部分字符(如“王**蛋”),根据
Match.Start和Match.End计算 rune 索引,只覆盖中间段
Gin 中间件里怎么安全读取并重置 c.Request.Body
读一次 io.ReadAll(c.Request.Body) 后,后续 c.ShouldBindJSON() 就收不到数据——body 已 EOF。这不是 bug,是 Go HTTP 的流式设计决定的。
立即学习“go语言免费学习笔记(深入)”;
- 必须用
bytes.NewBuffer(bodyBytes)包一层,再赋给c.Request.Body = io.NopCloser(...) - 只处理
application/json和application/x-www-form-urlencoded类型,跳过multipart/form-data(文件上传)和text/plain(日志上报) - 解析 JSON 后,递归遍历 map[string]interface{} 或 struct,只对
string字段调用过滤函数,避免误改字段名或数字 - 命中敏感词立即
c.AbortWithStatusJSON(400, ...)并 return,不要继续执行 handler
真正难的不是写一个能跑的 demo,是让每个 rune 对得上位置、每个 map[rune]*Node 不 panic、每次 body 重置都不丢字节——线上漏判一条,就是风险;误杀一次,就是投诉。这些点不提前踩一遍,压测时才暴露就晚了。

















