Go结构化日志不能只靠fmt.Printf,因其输出纯文本、无schema、字段边界模糊,导致日志平台解析困难;应使用zap或zerolog等支持键值对的库,按场景选型并规范字段命名与类型。

Go 结构化日志为什么不能只靠 fmt.Printf
因为 fmt.Printf 输出的是纯文本,字段边界模糊、无固定 schema,后续用 grep 或日志平台(如 Loki、ELK)做字段提取时,得靠正则硬匹配,一改格式就全崩。结构化日志的核心是:每个日志条目是一个可解析的键值对象,不是字符串拼接。
选 zap 还是 zerolog?关键看你的输出场景
zap 性能更高(尤其在高并发写文件时),但默认不支持直接输出 JSON 到 stdout(需配 zapcore.NewJSONEncoder);zerolog 默认就是 JSON 输出,API 更轻量,但字段名强制小驼峰(比如 req_id 不能写成 reqId),且不支持动态字段名(即 key 是变量时得绕路)。
- 如果你用 Kubernetes + Loki:选
zerolog,省去 encoder 配置,Loki 的logfmt和 JSON parser 都能直接识别 - 如果你写日志到本地文件且 QPS > 5k:用
zap,开启BufferedWriteSyncer能明显降 syscall 开销 - 如果你需要字段名完全可控(比如必须传
traceID而非trace_id):zap更灵活
zerolog 中如何避免字段污染和上下文丢失
全局 logger(如 zerolog.Logger)一旦用 With().Str("user_id", "123") 添加字段,后续所有日志都会带上它——这在 HTTP handler 中极易导致 A 用户的日志混进 B 用户的请求里。正确做法是:每个请求生命周期内,从基础 logger 派生出 request-scoped logger。
func handler(w http.ResponseWriter, r *http.Request) {
// 从全局 logger 派生,注入 request 级字段
log := zerolog.Ctx(r.Context()).With().
Str("method", r.Method).
Str("path", r.URL.Path).
Str("req_id", getReqID(r)).
Logger()
log.Info().Msg("request started")
// ... 处理逻辑
log.Info().Int("status", 200).Msg("request completed")
}
注意:zerolog.Ctx(r.Context()) 要求你在中间件中提前把 logger 注入 context(用 context.WithValue),否则会 panic。
立即学习“go语言免费学习笔记(深入)”;
结构化日志字段命名和类型该怎么定
字段名要统一、可预期,别出现 userID、user_id、uid 并存;数值类字段必须用对应类型(别全用 string),否则 Grafana 做直方图或 Loki 做 | unpack | histogram 会失败。
- 必带字段建议:
level(string)、time(ISO8601 string)、service(服务名)、req_id(请求 ID)、span_id(链路 ID) - HTTP 相关:
method(string)、status(int)、duration_ms(float64)、remote_ip(string) - 数据库操作:
db_query(string)、db_duration_ms(float64)、db_rows_affected(int) - 禁止字段:
error(应拆成err_msg+err_type+stack),避免和日志系统内置字段冲突
字段多了容易漏打,建议封装一个 LogFields struct 或用 zerolog.Dict() 统一构造。










