敏感词过滤中间件必须挂载在路由注册前,全局过滤用engine.Use(),局部过滤应绑定到Group()分组;需统一解析请求体(JSON/表单),用AC自动机构建词典复用,命中时结构化记录关键词而非全文,确保并发安全与日志可追溯。

敏感词过滤中间件该挂在哪一层?
Gin 的中间件必须在路由注册前注册,否则不会生效。常见错误是把 engine.Use() 放在 engine.GET() 之后,结果请求完全不经过过滤逻辑。
- 中间件注册必须在所有
engine.GET()、engine.POST()等路由定义之前 - 如果只对部分接口过滤,不要用
engine.Use()全局挂载,改用分组:group := engine.Group("/api"); group.Use(sensitiveFilter) - 注意中间件执行顺序:多个中间件按注册顺序依次进入,
ctx.Next()后续才执行后续中间件或最终 handler
怎么高效匹配敏感词而不拖慢请求?
暴力遍历关键词列表(strings.Contains 挨个扫)在词库超 1000 条时,单次请求可能增加几毫秒延迟,QPS 下降明显。推荐用 AC 自动机或前缀树(Trie),但 Gin 场景下更务实的做法是:
- 用现成轻量库如
github.com/wlcy/trade/comm/ahocorasick(纯 Go 实现,无 CGO,支持并发安全构建) - 构建一次词典后复用:
ac, _ := ahocorasick.NewAhoCorasick(keywords),全局变量或依赖注入,避免每次请求重建 - 匹配时用
ac.FindAllString(text),返回命中词列表,比正则快 3–5 倍,且不惧回溯爆炸 - 避免在中间件里做替换(如 * 号打码),只检测+记录;替换逻辑交给业务 handler,保持中间件职责单一
如何让过滤逻辑兼容 JSON 和表单请求?
Gin 默认只解析 ctx.PostForm("content") 或 ctx.GetPostForm("content"),但前端可能发 JSON:{"content":"这个有违禁词"},此时 PostForm 返回空字符串,导致漏检。
- 统一提取文本字段:先尝试
ctx.ShouldBindJSON(&payload),再取 payload 字段;失败则 fallback 到ctx.PostForm("content") - 更稳妥方式是读原始 body:
body, _ := io.ReadAll(ctx.Request.Body),然后用json.Unmarshal或url.ParseQuery解析,注意重放 body(需用ctx.Request.Body = io.NopCloser(bytes.NewReader(body))) - 不要直接修改
ctx.Request.Body后不恢复,否则后续ShouldBindJSON会读到空数据,报invalid character错误
为什么日志里总看不到真实触发词?
很多实现只写“含敏感词”,却不记录具体哪个词命中、出现在哪段文本里,排查时只能靠猜。
立即学习“go语言免费学习笔记(深入)”;
- 在
ac.FindAllString(text)结果非空时,立刻记录:log.Printf("sensitive hit: %v in %s", hits, text[:min(len(text), 100)]) - 避免记录完整原文(尤其含用户隐私字段),截断前 100 字足够定位
- 如果用结构化日志(如 zap),把
hits作为字段传入,方便后续检索统计 - 不要在生产环境用
fmt.Println打点,它锁 stdout,高并发下会成为瓶颈
实际部署时,词库热更新和并发安全最容易被忽略:AC 树实例本身是只读的,但如果你用 map 动态增删词,就必须加 sync.RWMutex;而多数人直接全局变量赋值,导致 goroutine 看到不一致状态。


















