日志中间件出错必须兜底:最外层加defer-recover并调用c.abort(),补500响应,完整记录panic堆栈;避免i/o阻塞,fallback生成request_id需无panic,日志写入失败不得影响http流程。

日志中间件本身出错,会导致整个请求链路丢失上下文,比业务错误更危险。 它不该 panic,也不该静默失败;一旦 log.Printf 或结构化日志写入失败(比如 zap logger 关闭、磁盘满、io pipe 断开),必须确保不影响 HTTP 响应流程,且错误本身要可追溯。
日志中间件里 panic 了怎么办
日志中间件最常踩的坑,是调用未初始化的 logger、在 defer 中访问已释放的资源、或对空指针解引用(比如 c.Request 在某些测试 mock 下为 nil)。这类 panic 不会自动被捕获,会直接中断中间件链,导致后续 handler 不执行,响应不写出,连接挂起。
- 必须在日志中间件最外层加
defer func() { recover() },且recover()后要主动调用c.Abort(),防止控制流继续向下 - 不要只打印 panic 日志就完事——得补一个兜底响应,例如
c.JSON(500, gin.H{"msg": "service unavailable"}) - panic 堆栈必须记录完整,推荐用
fmt.Printf("%+v", err)或debug.PrintStack(),而不是err.Error() - 注意:如果日志中间件里启了 goroutine 做异步写入,goroutine 内 panic 是捕不到的,这种场景应单独封装带 recover 的 worker
logger.Write 失败时怎么保响应不丢
结构化日志库(如 zap)的 Logger.Info 等方法默认不返回 error,但底层 I/O 可能失败。你不能假设“写了就一定成功”,尤其在容器环境磁盘满、stdout 被重定向到挂掉的 fluentd 时。
- 避免在中间件中直接调用可能阻塞或失败的同步写入,比如
os.Stdout.WriteString - 使用 zap 的
AtomicLevel+Core组合时,可在自定义WriteEntry中判断err != nil,并降级到fmt.Fprint(os.Stderr, ...) - 更稳妥的做法:日志中间件只负责构造 log entry 结构体(含
request_id、status、cost等字段),真正写入交给独立 goroutine 或 buffer pool,中间件内绝不让 I/O 错误影响c.Next() - 如果用了
logrus,注意它默认 panic on write error,需提前设置logrus.ExitFunc = os.Exit改为logrus.ExitFunc = func(int) {}防进程退出
request_id 生成失败导致日志断链
很多日志中间件从 X-Request-Id header 读取 ID,但用户不带、网关没透传、或 header 被中间代理截断时,fallback 逻辑若出错(比如 time.Now().UnixNano() 返回负数、strconv.FormatInt panic),就会让整条日志缺失关键串联字段。
- fallback 生成必须无 panic:用
rand.Int63()+ 时间戳拼接,比UnixNano()更稳 - 不要把
request_id存进c.Keys后就不管——后续中间件或 handler 若覆盖同 key,会污染日志上下文 - 建议统一用
c.Set("request_id", id)+ 显式传参给 logger,而非依赖全局 context.Value,减少隐式依赖 - 在日志输出前加校验:
if reqID == "" { reqID = "unknown" },至少保证字段不为空,避免日志系统解析失败
真正难的不是加一行 log.Printf,而是让日志中间件在自身出问题时,既不拖垮请求,也不掩盖问题——它得有自愈力,也得留痕迹。线上最怕的不是错误,是错误发生后你连它在哪都不知道。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











