用zap替代log包是实现结构化日志的前提,因其支持JSON/键值对输出、高性能及lumberjack集成实现日志轮转,而标准log包仅支持纯文本且无字段化能力。

用 zap 替代 log 包是结构化日志的前提
Go 标准库的 log 包输出纯文本,没法直接打结构化字段(比如 "user_id":123,"action":"login")。必须换支持 JSON 或键值对的 logger,zap 是目前最常用、性能最好、且官方推荐的选择。别用 logrus 或 zerolog 的变体做默认方案——它们在高并发写文件时容易卡住或丢日志,zap 的 core 设计和异步写入机制更稳。
安装:
go get -u go.uber.org/zap
初始化一个带字段的日志实例,不是简单调 zap.NewProduction(),而是要自己构建 zap.Config,否则无法控制文件路径和滚动逻辑。
配置 zap 写入滚动文件要用 lumberjack 而非原生 os.File
zap 本身不提供文件滚动能力,直接用 os.OpenFile 写死路径会导致日志文件无限增长,最终撑爆磁盘。必须桥接 lumberjack 这个第三方轮子(它被 zap 官方文档明确推荐)。
立即学习“go语言免费学习笔记(深入)”;
关键点:
-
lumberjack.Logger要设置MaxSize(单位 MB)、MaxBackups、MaxAge(天),三者共同决定何时切文件、保留几个旧文件、旧文件最多存多久 - 不能把
lumberjack.Logger直接传给zap.NewCore,得先包装成io.WriteSyncer:用zapcore.AddSync封装 - 注意
lumberjack的LocalTime: true,否则归档文件名里的时间是 UTC,排查时容易看错
示例片段:
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func newZapLogger() (*zap.Logger, error) {
w := zapcore.AddSync(&lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 50, // MB
MaxBackups: 7,
MaxAge: 28, // days
LocalTime: true,
})
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{"stdout", "logs/app.log"} // 控制哪些地方写(可同时 stdout + file)
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cfg.EncoderConfig.LevelKey = "level"
cfg.EncoderConfig.TimeKey = "ts"
core := zapcore.NewCore(
zapcore.NewJSONEncoder(cfg.EncoderConfig),
w,
zapcore.InfoLevel,
)
return zap.New(core), nil
}
在 Gin / Echo 等框架中注入自定义 zap 实例要绕过中间件生命周期陷阱
很多人把 zap.Logger 塞进 context.Context,然后每个 handler 里用 c.Request().Context() 取——这看似合理,但实际会漏掉中间件自身产生的日志(比如认证失败、CORS 预检失败),因为那些错误发生在请求上下文创建之前。
正确做法:
- Gin:用
gin.WithValue("logger", logger)注册全局值,再写一个中间件,在c.Next()前后用c.MustGet("logger").(*zap.Logger)打日志;别依赖 context 传递 - Echo:注册
echo.HTTPErrorHandler和echo.HTTPErrorHandler时,显式传入 logger 实例,而不是从c.Request().Context()拿 - 所有框架都要确保 logger 实例是单例(
init()或var全局声明),避免每次请求都新建,否则滚动文件句柄会泄漏
额外提醒:zap 的 Sugar() 方法虽方便(支持 sugar.Infow("msg", "key", val)),但在高频日志场景下比 With() + Info() 慢 20% 左右,生产环境建议统一用结构化方式,别混用。
滚动文件权限、路径和多进程写入问题最容易被忽略
Linux 下常见问题:日志目录不存在、权限不足导致 open logs/app.log: no such file or directory 或 permission denied;Windows 下路径分隔符写成 / 没事,但用 \ 不加转义会编译报错。
更隐蔽的是多实例部署(比如用 systemd 启多个进程,或 Docker 多副本)时,lumberjack 无法跨进程协调滚动,会出现多个进程往同一个文件写、归档错乱、甚至文件被 truncate。
解决办法只有两个:
- 强制单实例运行(加
file lock,但复杂且有竞态) - 改用集中式日志方案(如
syslog、fluentd收集,再落地到文件系统)——这是线上真实做法,lumberjack仅适合单机调试或低流量服务
如果坚持本地滚动,至少确保日志路径是绝对路径,并在程序启动时用 os.MkdirAll 创建目录并 os.Chmod 设权限,别指望运维手动配。


















