Go 1.21+ 应统一使用官方结构化日志 slog,避免手动拼接、上下文丢失;需初始化 JSON Handler、显式注入 trace_id 等字段、主动记录而非依赖 ErrorLog、用 slog.Any 保留错误链、精准分级避免告警淹没。

Go 1.21+ 的 slog 是官方结构化日志标准,直接用它就能避免手动拼接、上下文丢失、解析失败等问题;标准库 log 不支持级别和字段,硬改格式只是绕弯子。
用 slog 初始化带 trace_id 和 level 的 JSON 日志
别再封装 log.Printf 或写自定义 io.Writer 拦截 http.Server.ErrorLog —— 那是 Go 1.20 之前的权宜之计。现在统一用 slog,初始化一次,全局复用:
-
slog.NewJSONHandler(os.Stdout, nil)输出可被 Loki/ELK 直接解析的 JSON;生产环境必须显式指定,不能依赖默认文本格式 - 用
slog.With()注入服务名、环境、启动参数等静态字段,返回新Logger实例,不可变且线程安全 - HTTP handler 中通过
context.Context提取trace_id,再调用reqLogger := logger.With(slog.String("trace_id", traceID)),不是在每条日志里重复传 -
slog.String("key", value)必须成对出现,不能漏掉value;字段名禁用空格、点号、斜杠,否则filebeat等采集器解析失败
在 http.Server 中替换默认 ErrorLog 时的常见错误
很多人想“定制 HTTP 错误日志格式”,结果去包装 io.Writer 改 Server.ErrorLog 的输出——这只能加时间戳或前缀,但无法注入 request_id、path、status 等关键上下文。真正该做的是:不依赖 ErrorLog,改用 slog 主动记录。
-
http.Server的ErrorLog只用于底层监听错误(如端口占用、TLS 握手失败),不是业务错误出口;95% 的 “400/500 错误” 应由 handler 自己用slog.Error记录 - 若真要捕获监听层错误(比如
accept tcp: too many open files),可传入自定义io.Writer,但仅作兜底,不承载结构化字段 - 不要用
log.SetOutput全局覆盖——它是副作用,多个包调用会互相干扰;slog的Logger实例是显式传递的,无此风险 - 示例错误写法:
server := &http.Server{ErrorLog: log.New(customWriter, "", 0)}→ 此时你仍拿不到请求上下文,日志价值极低
记录 error 时保留错误链和调用位置的实操要点
只传 err.Error() 就等于扔掉所有诊断线索。Go 错误链(%w)和栈帧信息必须在构造阶段注入,日志库才能提取。
立即学习“go语言免费学习笔记(深入)”;
- 业务代码中禁止:
return errors.New("db timeout")或log.Println(err);必须用fmt.Errorf("query user %d: %w", id, err) - 使用
slog.Error("failed to query", slog.Any("err", err))——slog.Any会自动展开Unwrap()链,比slog.String("err", err.Error())强一个数量级 - 不想暴露原始堆栈?用
slog.String("err_type", fmt.Sprintf("%T", err))替代,但别丢掉errors.Is()判断能力 - panic 场景下,
recover()后必须调用debug.PrintStack(),且日志要带slog.String("panic", fmt.Sprint(r)),否则进程退出前缓冲区未 flush,日志就丢了
真正的难点不在“怎么打日志”,而在于“什么时候不该打”:重试中的中间错误、已知的客户端参数错误(如 json.Unmarshal 失败但已返回 400)、健康检查失败——这些都不该进 ERROR 流,否则告警会被淹没。日志不是越多越好,是越精准越好。


















