Go mod 好菜系列 - 0x04 zap 让日志终于像日志
详细讲 zap 为什么在 Go 项目里很常见、结构化日志怎么写、字段日志有什么价值,以及它通常应该放在哪一层。
很多人刚开始写 Go 服务时,日志基本靠 fmt.Println 和 log.Println 撑着。小项目当然能跑,但只要一上环境、接口一多、并发一高,你很快就会发现:日志不只是“有没有输出”,而是“能不能拿来排查问题”。
zap 为什么常见
因为它在 Go 生态里属于那种比较典型的“工程味日志库”:
- 结构化
- 性能不错
- 输出格式清晰
- 生态使用广
也就是说,它不是为了让日志看起来花,而是让日志更容易被程序和人一起消费。
结构化日志到底好在哪
先看普通文本日志:
user login failed, id=42, ip=1.2.3.4看着也不是不能懂,但如果你想按用户 id、按 IP、按错误类型去筛选,它就开始显得不够规整了。
结构化日志更像这样:
logger.Info("user login failed",
zap.Int("user_id", 42),
zap.String("ip", "1.2.3.4"),
zap.String("reason", "password mismatch"),
)这样输出以后,不管是你自己 grep,还是日志平台做索引分析,都更容易。
最常见的初始化
logger, err := zap.NewProduction()
if err != nil {
return err
}
defer logger.Sync()NewProduction() 基本就是“先上车再说”的那种默认方案。开发环境也可以用 zap.NewDevelopment(),会更适合人肉阅读。
字段日志怎么理解
zap 最有代表性的就是那组字段函数:
zap.Stringzap.Intzap.Durationzap.Error
logger.Info("request done",
zap.String("path", "/api/users"),
zap.Int("status", 200),
zap.Duration("cost", 35*time.Millisecond),
)这种写法在中间件、数据库耗时、第三方接口调用这些场景里特别有用。它不是在写文章,而是在给未来的排查留抓手。
错误日志别只打一串 err
if err != nil {
logger.Error("create user failed",
zap.Error(err),
zap.String("email", req.Email),
)
return err
}单纯把 err 打出来当然也能看,但带上一点关键上下文,价值会高很多。以后你遇到线上问题,往往不是缺“错误发生了”这个事实,而是缺“它是带着什么条件发生的”。
zap 最适合放在哪
它其实是个横切工具,不是某一层专属的。比较常见的用法是:
- 服务启动时初始化一个全局或注入式 logger
- 中间件里打请求日志
- service / repository 层打关键错误和耗时
- 后台任务也统一用它
核心原则不是“哪里都打”,而是“关键节点打得足够有信息量”。
常见坏味道
- 日志太多,全是噪音
- 字段名乱写,后期分析全靠猜
- 业务日志和错误日志混成一锅
- 既然是结构化日志,最后 message 还写成一整段小说
日志不是越多越专业,越能回答问题才越专业。
小结
zap 常见不是因为它酷,而是因为它很适合现代服务里的日志需求:
- 结构化字段比纯文本日志更适合排查和分析
- 初始化简单,性能也比较能打
- 日志要记录上下文,而不是只喊一句“出错了”
- 它适合做全项目统一日志方案的一部分
下一篇我们讲 viper。日志说人话以后,配置也该像样一点,不要再到处散着环境变量和写死常量了。