绝大多数场景应使用gin.default(),因其默认集成logger和recovery中间件,提供基础日志与panic恢复能力;生产环境需精细控制日志输出、错误处理及安全策略时,则必须用gin.new()手动注册中间件。

直接用 gin.Default() 就没法换日志库——它把 Logger() 和 Recovery() 中间件硬编码进去了,必须用 gin.New() + 手动 Use() 才能接管日志输出逻辑。
为什么不能直接替换 gin.Default() 的日志
gin 的 Default() 函数内部调用的是 Engine.Use(Logger(), Recovery()),而它的 Logger() 中间件固定写死到 os.Stdout,不支持注入自定义 *zap.Logger。强行 patch 或 monkey patch 会破坏 Gin 的可维护性,且无法传递请求级字段(如 req_id)。
-
gin.Default()是开箱即用的快捷方式,不是可扩展入口 - 所有基于
context.WithValue注入 logger 的中间件,都必须在gin.New()后显式Use() - 官方推荐做法:自己实现
GinLogger和GinRecovery,而非复用内置版本
如何写一个线程安全、带请求 ID 的 Zap 中间件
核心是避免每次请求都调用 logger.With() 创建新实例,同时确保 WriteSyncer 被锁保护。字段合并本身有内存分配开销,高频请求下 GC 压力明显。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 预置通用字段(
service_name、env、host)在全局*zap.Logger初始化时就加上,不要拖到中间件里 - 请求级字段(
req_id、path、method)用logger.With()一次生成,再传给c.Set()或结构体 receiver - 务必用
zapcore.Lock(zapcore.AddSync(file))包裹文件写入器,裸*os.File在多 goroutine 下会 panic - 别在中间件里调
logger.Sync()—— 它阻塞整个请求链,只应在进程退出前调一次
字段命名不一致会导致日志平台查不到数据
Zap 默认字段名是小驼峰(httpStatus、reqId),但 Loki、Datadog、阿里 SLS 等平台默认按蛇形(http_status、req_id)建索引。字段名对不上,Kibana 搜索 status:500 就永远为空。
- 必须重写
EncoderConfig,把LevelKey、MessageKey等全部显式设为下划线风格 - 例如:
LevelKey: "level"、TimeKey: "ts"、CallerKey: "caller"、StacktraceKey: "stacktrace" - 切忌依赖默认值,Zap 的
LowercaseLevelEncoder只改大小写,不改分隔符
采样机制会让高频日志静默丢弃
zap.NewProduction() 默认启用采样(sampling),每秒超过 100 条同一条日志(相同 msg + 相同字段)就会被自动丢弃。HTTP 请求 trace、健康检查日志、重试循环日志极易中招。
- 关闭采样:
zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewSampler(core, time.Second, 1000, 1000) }),或直接用zap.NewDevelopment()(无采样) - 更稳妥的做法是:用
zapcore.NewCore手动构造 core,把zapcore.NewSamplerCore替换为无采样 wrapper - 注意:禁用采样后,磁盘 I/O 和缓冲区压力会上升,需配合异步 write syncer(如
lumberjack)和合理 rotate 配置
最易被忽略的一点:Zap 的性能优势几乎全靠初始化配置兑现。漏掉 zap.AddCaller()、没锁 WriteSyncer、时间格式化走 time.Now().Format(),吞吐量立刻跌回标准库 log 水平——这些都不是运行时能动态补救的。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










