标准库 log 仅适用于本地 CLI、单元测试或短时 Job;生产环境必须用 zap/zerolog,因 log 会引发性能瓶颈和运维盲点;logrus 已归档不建议新项目使用;zap 是事实标准,因其零分配、结构化、易集成等优势。

log 适合原型验证或单机小工具;生产服务、高并发 API、需结构化日志的场景,必须用 zap 或 zerolog —— 不是“更好用”,而是 log 在这些场景下会成为性能瓶颈和运维盲点。
什么时候还能用标准库 log?
仅限以下情况:
- 本地 CLI 工具(如 go run main.go 一次性脚本)
- 单元测试中的临时日志输出(t.Log() 更合适)
- 容器内只运行几秒的 Job 类任务,且不依赖日志排查问题
- 你明确接受:无日志级别控制、无法异步写入、每条日志都触发一次系统调用、GC 压力随日志量线性增长
logrus 现在还能不能上新项目?
不建议。关键事实:
- 官方仓库自 2023 年起已归档(archived),不再接受 PR、不修复 CVE
- 虽然仍能 go get,但其 Hook 机制与现代日志采集链路(如 filebeat → Kafka → Loki)耦合重、扩展难
- 日志字段序列化依赖反射,高并发下易成 CPU 热点(对比 zap 的预分配 encoder)
- 如果已有老项目在用,可继续维护;但新项目请直接跳过
为什么 zap 是当前生产环境事实标准?
它解决的是真实部署痛点,不是纸面参数:
- zap.NewProduction() 默认启用 JSON 编码 + 时间戳 + 调用栈裁剪 + sync.Pool 缓冲复用,开箱即接入 promtail/fluentd
- logger.With() 返回新 logger,支持 request-scoped 上下文注入(比如自动带 req_id、user_id),避免到处传参
- logger.Debug() 在非 Debug 模式下**零分配、零字符串拼接**,而 logrus.Info("user=", u.ID, "action=", a) 每次都触发 fmt.Sprint
- 支持 WriteSyncer 接口,可无缝对接 lumberjack.Logger 实现轮转,或自定义写入 /dev/stdout + systemd-journald 标签
package mainimport ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" )
func main() { w := zapcore.AddSync(&lumberjack.Logger{ Filename: "/var/log/myapp/app.log", MaxSize: 100, // MB MaxBackups: 7, MaxAge: 28, // days }) core := zapcore.NewCore( zapcore.NewJSONEncoder(zapcore.EncoderConfig{ TimeKey: "ts", LevelKey: "level", NameKey: "logger", CallerKey: "caller", MessageKey: "msg", EncodeTime: zapcore.ISO8601TimeEncoder, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeCaller: zapcore.ShortCallerEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, }), w, zapcore.InfoLevel, ) logger := zap.New(core) defer logger.Sync()
logger.Info("service started", zap.String("version", "v1.2.0"))}
容易被忽略的部署细节
Linux 下日志路径和权限常导致静默失败:
- 不要硬写/var/log/xxx.log,除非你的systemdservice 文件里明确配置了ReadWritePaths=/var/log/xxx
- 容器中优先用os.Stdout,靠docker logs或kubectl logs统一收集;轮转交给宿主机日志代理
-zap的logger.Sync()必须调用(尤其进程退出前),否则最后几条日志可能丢失 —— 这个坑在压测时才暴露
- 若用zerolog,注意它默认不输出时间戳,需手动加.Timestamp(),否则loki查询会丢时间维度










