golang的日志库zap
日志作为整个代码行为的记录,是程序执行逻辑和异常最直接的反馈。对于整个系统来说,日志是至关重要的组成部分。通过分析日志我们不仅可以发现系统的问题,同时日志中也蕴含了大量有价值可以被挖掘的信息,因此合理地记录日志是十分必要的。
1. golang log libs
目前golang主流的 log库有
- https://github.com/uber-go/zap
- https://github.com/Sirupsen/logrus
- 标准库
log/slog - https://github.com/rs/zerolog
zap 跟 logrus 以及目前主流的 go 语言 log 类似,提倡采用结构化的日志格式,而不是将所有消息放到消息体中,简单来讲,日志有两个概念:字段和消息。字段用来结构化输出错误相关的上下文环境,而消息简明扼要的阐述错误本身。
一个现代日志库通常可以拆成四个部分理解:
Logger:代码里直接调用的记录器,例如logger.Info("用户登录成功")。Level:级别控制器,决定低于当前级别的日志是否直接丢弃。Formatter:格式化器,决定输出为纯文本、JSON 或其他格式。Output:输出器,决定日志写到终端、文件还是网络服务。
zap 功能完整、性能强,适合生产服务;zerolog 更极致和简洁;slog 是 Go 标准库提供的结构化日志接口,适合作为应用和第三方库之间的统一日志抽象。库作者可以面向 slog 记录日志,应用开发者在 main 函数里决定使用标准 Handler,或者接入 zap、zerolog 的 slog 适配器,从而统一整个应用的日志格式。
1.1 log库对比
1 | package main |
2. zap 使用
zap 是一个由 Uber 开源的、为性能而生的 结构化 日志库。它在提供极高速度的同时,也让日志变得更易于机器解析和查询。
在你的日常工作中,可能经常使用 fmt.Println 或标准库 log。但在大型、高并发的生产环境中,它们存在两个致命问题:
- 性能黑洞:标准库的日志记录大量使用
fmt.Sprintf和反射(reflection),这在每秒需要处理成千上万次请求的系统中,会成为显著的性能瓶颈,白白消耗大量 CPU 资源。 - 日志孤岛:传统的文本日志对人类友好,但对机器极不友好。当系统出现问题,你需要在海量(可能是 GB 甚至 TB 级别)的日志文件中搜索特定信息时,
grep命令会变得缓慢且低效。你无法轻松地进行聚合分析,比如“统计过去一小时内,由用户 ID 123 触发的错误总数”。
zap 完美地解决了这两个问题:
- 极致性能:它通过避免反射、使用对象池 (
sync.Pool) 等技术,最大限度地减少了内存分配和计算开销,是目前 Go 生态中最快的日志库之一。 - 结构化日志:它默认输出 JSON 格式的日志,每个日志条目都是一个键值对集合。这使得日志可以被 Elasticsearch、Splunk、Datadog 等日志聚合平台轻松地索引、搜索和可视化,让故障排查和数据分析的效率提升一个数量级。
学习 zap,就是学习如何构建一个现代化的、可观测的(Observability)高性能系统。
2.1 工作原理
Logger&SugaredLogger(用户接口)Logger:性能最高的API。它要求你明确提供强类型的字段(Field),例如zap.String("key", "value"),zap.Int("userID", 123)。这是zap性能的保证。SugaredLogger:语法糖版本,性能略低于Logger,但API更友好,类似于fmt.Printf的风格,如sugar.Infof("Failed to fetch user: %s", err)。适合从传统log库迁移或非性能瓶颈的场景。
zapcore.Core(核心处理器)
这是zap的心脏,它告诉Logger一个日志条目应该如何被处理。一个Core需要三个要素:Encoder:编码器。决定日志的格式。zap内置了jsonEncoder(输出 JSON) 和consoleEncoder(输出对人友好的彩色文本)。你也可以自定义。WriteSyncer:写入器。决定日志要被写到哪里。可以是标准输出 (os.Stdout)、文件,甚至可以同时写入多个地方。LevelEnabler:级别控制器。决定哪个级别的日志应该被记录(例如,只记录InfoLevel及以上级别的日志)。
Field(结构化字段)
这是zap区别于传统日志库的根本。每一个Field都是一个预先定义好类型的键值对。由于类型已知,zap在编码时无需使用反射,从而实现了零内存分配和极高的效率。
2.2 注意事项
始终使用结构化日志:不要把关键字段拼进字符串里。例如不要写
logger.Errorf("用户 %d 登录失败", userID),而要记录成logger.With(zap.Int64("user_id", userID)).Error("登录失败")。这样user_id才能被日志平台检索和聚合。上下文信息很关键:Web 请求里应该把
request_id、user_id、trace_id等字段注入到 Logger 上,后续所有日志都继承这些字段,排查问题时才能串起完整链路。不要记录敏感信息:密码、身份证号、手机号、API Secret Key 等都不应该原样写入日志。确实需要排查时,也要先做脱敏。
日志级别要稳定:
Info写业务关键路径,Warn写可恢复但需要关注的问题,Error写请求或任务失败。不要把正常分支写成Error,否则告警会失真。在
main或初始化阶段配置一次 Logger:通常创建全局 Logger 或通过依赖注入传递,不要在每个函数里重复创建新的 Logger。优先使用
Logger:在对性能有要求的代码路径中(比如请求处理的核心逻辑),坚持使用zap.Logger而不是zap.SugaredLogger。把SugaredLogger当作便利性的权衡。善用
With创建上下文 Logger:当处理一个请求时,可以创建一个带有共同字段(如request_id,user_id)的子logger。这样后续的日志就不用重复添加这些字段了。1
2
3
4
5
6
7
8// 在中间件或请求开始时
contextualLogger := logger.With(
zap.String("requestID", "xyz-123"),
zap.String("userID", "u-456"),
)
contextualLogger.Info("Processing started")
// ... 在其他函数中使用 contextualLogger处理
error类型:记录error时,使用zap.Error(err)而不是zap.String("error", err.Error())。zap.Error字段会对实现了特定接口的错误(如zap.StackTracer)进行特殊处理,自动附加堆栈信息,非常便于调试。整个项目都使用
sugar.Infof("user %s logged in", user.ID),然后奇怪为什么性能提升不明显。Infof,Warnf等方法内部仍然有类似fmt.Sprintf的开销,这违背了zap的设计初衷。在
zap.Any中使用复杂类型。zap.Any会触发反射,带来性能开销。此外,如果该struct没有实现json.Marshaler接口,输出的日志可能会非常混乱或不符合预期。
2.3 使用代码
- sugar模式 (牺牲性能为代价,增强可用性)
1 | func main() { |
- logger 模式
1 | func main() { |
2.4 输出到文件里
1 | func NewLogger() (*zap.Logger, error) { |
2.5 输入到滚动文件里
Lumberjack用于将日志写入滚动文件。zap 不支持文件归档,如果要支持文件按大小或者时间归档,需要使用lumberjack,lumberjack也是zap官方推荐的。https://github.com/natefinch/lumberjack
1 | func main() { |
3. 提问问题
3.1 NewDevelopment 和 NewProduction 区别
| 特性 | zap.NewDevelopment() | zap.NewProduction() | 设计目的 |
|---|---|---|---|
| 日志格式 (Encoder) | consoleEncoder (彩色、多行、对人友好) | jsonEncoder (单行 JSON,对机器友好) | 开发时需要快速扫视,生产环境需要日志聚合平台能轻松解析。 |
| 默认日志级别 | DebugLevel (非常详细) | InfoLevel (更收敛) | 开发时希望看到所有信息,生产环境只关心正常流程和错误,避免日志泛滥。 |
| 调用者信息 (Caller) | 启用 (file:line) | 启用 (file:line) | 无论何种环境,知道日志来源都非常重要。 |
| 堆栈信息 (Stacktrace) | WarnLevel 及以上级别自动附加 | ErrorLevel 及以上级别自动附加 | 开发时,连一个警告(Warn)都希望看到完整的调用栈;生产环境,只有真正的错误(Error)才值得记录昂贵的堆栈信息。 |
| 时间格式 | ISO8601 (例如 2025-08-14T15:04:05.000Z0700) | Unix 时间戳 (例如 1755193445.000) | 人眼对 ISO8601 更友好,而时间戳对机器来说处理效率更高、存储更紧凑。 |
| 日志采样 (Sampling) | 禁用 | 启用 | 这是 Production 配置的一个关键特性。它可以在极高的日志吞吐量下,防止日志风暴。例如,它会确保每秒内相同的、重复的日志只记录前 100 条,之后每 100 条中再记录 1 条。这能极大地保护你的系统和日志平台不被冲垮。 |
3.2 怎么带有 trace_id
通过中间件注入
1 | package main |