gin中间件比装饰器更适合操作日志,因其天然契合aop环绕通知语义,避免手动包装handler;需正确使用c.next()前后时机记录请求响应、defer/recover捕获panic、用io.teereader缓存并复用body流、敏感字段脱敏、关键字段强制记录、异步写入防阻塞、注入traceid保障上下文。

为什么 Gin 的中间件比装饰器更适合做操作日志
因为 Gin 本身是基于中间件链的 HTTP 路由框架,gin.HandlerFunc 天然符合 AOP 中“环绕通知”的语义,而 Go 没有运行时反射增强(如 Java 的 Spring AOP),硬套装饰器模式反而要手动包装每个 handler,维护成本高、易漏逻辑。
常见错误是试图用函数闭包层层嵌套 handler,结果 c.Next() 调用位置不对,导致日志里看不到响应耗时或状态码;或者在 panic 后没 recover,整个请求直接 500 且无记录。
- 必须在
c.Next()前记录请求开始时间、方法、路径、IP - 必须在
c.Next()后读取c.Writer.Status()和c.Writer.Size()获取真实响应状态与字节数 - 若 handler 内部 panic,需在中间件中用
defer/recover捕获,并显式写入错误信息
如何拿到完整请求参数(包括 POST body)而不破坏读取流
Gin 的 c.Request.Body 是单次可读流,直接 ioutil.ReadAll 会消耗掉它,后续绑定(如 c.ShouldBindJSON)失败,报错 invalid memory address or nil pointer dereference 或空结构体。
正确做法是用 c.Request.Body 的副本缓存原始数据,再替换回请求体。Gin 1.9+ 提供了 c.Request.GetBody,但更稳妥的是用 io.TeeReader + bytes.Buffer 拦截并复用:
var buf bytes.Buffer tee := io.TeeReader(c.Request.Body, &buf) bodyBytes, _ := io.ReadAll(tee) c.Request.Body = io.NopCloser(&buf) // 恢复可读性 // 此时 bodyBytes 可用于日志,c.ShouldBindJSON 仍能正常工作
- 仅对需要记录 body 的路由启用该逻辑(如
POST /api/user),避免全量解析影响性能 - 注意限制 body 大小(如
if len(bodyBytes) > 10240 { bodyBytes = bodyBytes[:10240] }),防止日志爆炸 - 敏感字段(如 password)需在写入日志前主动脱敏,不能依赖前端不传
日志结构里哪些字段必须存,哪些可以按需开关
操作日志不是越全越好,关键是要能快速定位问题:谁、什么时间、调了什么接口、输入是什么、结果是否成功、耗时多少。以下字段建议强制记录:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
-
method(c.Request.Method) -
path(c.Request.URL.Path) -
status(c.Writer.Status()) -
cost_ms(time.Since(start).Milliseconds()) -
client_ip(优先用c.ClientIP(),非c.Request.RemoteAddr) -
user_id(从 token 或 session 解析,若未登录可填"anonymous")
以下字段建议配置化开关:
- 请求/响应 body(默认关,调试时开)
- query string(
c.Request.URL.RawQuery,含敏感参数风险) - header(如
Authorization必须过滤)
并发写日志时 panic 或丢日志?用 sync.Pool + 异步队列更稳
高频接口下,每请求都调 log.Printf 或直写文件,容易因 I/O 阻塞拖慢整个 HTTP 响应。更糟的是多个 goroutine 同时写同一个 *os.File,若没加锁可能 panic 或内容错乱。
简单方案是用 sync.Pool 复用日志结构体 + chan 异步投递:
type LogEntry struct {
Method, Path, UserID string
Status, CostMs int
ClientIP string
Body []byte
}
logChan := make(chan *LogEntry, 1000)
go func() {
for entry := range logChan {
// 写入文件 / 发送到 Kafka / 转成 JSON 打印到 stdout
_ = json.NewEncoder(os.Stdout).Encode(entry)
}
}()
// 中间件里:logChan
- channel 缓冲区大小设为 1000 是经验值,太小易阻塞,太大占内存
- 务必启动独立 goroutine 消费 channel,否则中间件会卡住
- 如果用第三方日志库(如 zap),直接调
logger.Info()即可,它内部已做异步和对象池优化
真正难处理的是日志上下文丢失——比如一个请求触发了三次 DB 查询,但日志里看不出它们属于同一次调用。这时候得靠 traceID,而 Gin 默认不带,必须在最外层中间件生成并注入 c.Set("trace_id", uuid.New().String()),后续所有日志都要带上它。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










