Golang结构化日志配置关键在初始化顺序与环境变量:zerolog需先设TimeFieldFormat和SetGlobalLevel,zap必须配EncoderConfig并调用Sync(),且GOROOT、PATH、GOPROXY三环境变量缺一不可。

直接上结论:Golang 环境本身几行命令就能搭好,真正卡住人的是结构化日志——zerolog 不设 TimeFieldFormat 和 SetGlobalLevel 顺序,zap 不配 EncoderConfig 或漏掉 Sync(),日志就等于没写。
Go 环境装完就跑不起来?先盯死这三个环境变量
不是装了 Go 就万事大吉。远程服务器或 Windows 上常见问题:go version 能跑,但 go run 报 command not found 或模块拉不到包,八成是环境变量没生效。
-
GOROOT必须指向 Go 安装根目录(如/usr/local/go或C:\Program Files\Go),不能错位 -
PATH里必须包含$GOROOT/bin,否则go命令找不到 -
GOPROXY必须设为国内镜像,比如https://goproxy.cn,direct;不设的话go get十次九超时
验证方式:开新终端,执行 go env | grep -E "(GOROOT|PATH|GOPROXY)",三者都得有值且路径可访问。
zerolog 初始化必须在 main() 第一行干这三件事
zerolog 不是“创建 logger 就能用”,它默认输出纳秒时间戳、无 level 字段、不校验类型——Loki 查不到、Grafana 画不出图,根源都在初始化漏步骤。
立即学习“go语言免费学习笔记(深入)”;
-
zerolog.TimeFieldFormat = time.RFC3339:必须在zerolog.New(...)之前设,否则时间字段是整数,不是字符串,Loki 解析失败 -
zerolog.SetGlobalLevel(zerolog.InfoLevel):必须放在import后、任何log.Info().Msg()前,通常就是func main()的第一行 - 静态字段只能靠
.With().Timestamp().Str("service", "api").Logger()注入,.Fields(map[string]interface{})会把整个 map 当字符串塞进一个 key,Loki 当纯文本丢弃
错误示范:log := zerolog.New(os.Stdout); log.Info().Msg("start") → 没时间、没 level、没 service,下游系统全丢。
zap 开发和生产配置不能混用,JSON 和 console 编码器行为差异极大
zap.NewProduction() 默认不打 Debug 日志、输出 JSON 到 os.Stderr、没换行没颜色——你盯着终端看半天以为没输出,其实是被当成一整行 JSON 吞了。
- 开发阶段用
zap.NewDevelopment():自动带颜色、文件行号、允许Debug(),不用额外配 - 生产阶段若坚持用
zap.NewProduction(),必须显式加等级判断:zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.InfoLevel }) - 无论哪种配置,进程退出前必须调
logger.Sync(),否则缓冲区日志丢失;defer logger.Sync()是最稳妥写法
字段写法也容易翻车:logger.Info("msg", zap.String("user_id", "123")) 正确,logger.Info("msg", "user_id", "123") 会 panic —— zap.Logger 不接受松散键值对。
HTTP 中间件里动态加 trace_id,别碰全局 logger
静态字段(service、env)设一次就够了,但每个请求的 trace_id、method、status 必须动态注入,而且不能拼字符串。
- 从
r.Header.Get("X-Trace-ID")提取,或用uuid.NewString()生成,存进context.Context - 中间件里派生子 logger:
logger := log.Ctx(r.Context()).With().Str("trace_id", tid).Str("method", r.Method).Logger() - 记录响应时用
logger.Info().Int("status", w.Status()).Dur("duration", time.Since(start)).Msg("http request"),别写Msg("status=200 duration=12ms")—— 后者字段全丢,只剩一条字符串
最常被忽略的一点:所有动态字段名必须是顶层 key,不能嵌套。比如 .Fields(map[string]interface{}{"req": map[string]string{"path": "/a"}}),Loki 只认 req 这个 key,里面内容当字符串处理,无法做 req.path = "/a" 查询。


















