Go 日志

从 log 到 log/slog 的结构化日志:级别、Handler、字段、与 context/trace 关联,以及生产环境脱敏、动态级别与性能边界。

#type / concept #status / growing #tech / dev #tech / dev / backend #resource / go

[!info] 关联笔记

Go 日志

这个概念为什么出现

服务出了问题,你需要在海量并发请求里回答:

  • 发生了什么、在哪个请求上
  • 关键上下文是什么(user、order、latency)
  • 能否被检索系统高效过滤
  • 能否在不重启的情况下临时提高详细度
  • 会不会因为日志本身拖垮延迟与 GC

老式 log.Printf 字符串拼接能救脚本,救不了生产。Go 1.21 起标准库 log/slog结构化、可级别过滤、可替换 Handler 变成默认路径;第三方 zerolog/zap 仍在极致性能与生态里占位。

[!abstract] 一句话理解 生产日志应结构化(键值字段 + 级别 + 时间 + msg),用可替换 Handler 输出 JSON/文本;通过 Logger.With/context 关联请求,并严禁写入密钥与未脱敏 PII。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:下单接口打可检索的结构化访问日志

订单服务启动时要写一条进程级启动日志;
每个 POST /orders 成功后,还要带着 request_idorder_id、耗时写一条业务日志,
方便在 Loki/ELK 里按请求串联排查。

slog 的 JSON Handler + Logger.With 就是把「固定字段」绑在请求域 Logger 上,避免每行手写拼接。

package main

import (
	"context"
	"log/slog"
	"os"
	"time"
)

// handleCreateOrder 模拟“创建订单成功后的日志侧写”。
//
// 业务意图:
// - 入口已有 request_id / route;
// - 落库成功后补 order_id 与 latency;
// - 字段必须是键值,不能是 printf 糊成一串。
//
// 教学点:
// - With 产生子 Logger,固定字段自动出现在后续每条日志;
// - Duration 等类型化 Attr 便于索引与聚合。
func handleCreateOrder(base *slog.Logger, requestID, orderID string) {
	// 请求域 Logger:中间件解析出的 request_id / route 在此挂上。
	reqLog := base.With(
		slog.String("request_id", requestID),
		slog.String("route", "POST /orders"),
	)

	start := time.Now()
	// 假装业务:写库、发事件……这里只演示日志落点。
	// 真实 handler 会在 return 前打成功/失败日志。
	reqLog.Info("order created",
		slog.String("order_id", orderID),
		slog.Duration("latency", time.Since(start)),
	)
}

func main() {
	// 生产默认:JSON 到 stdout,由采集侧收走。
	handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo, // 低于 Info 的 Debug 在 Handler 侧直接丢弃
	})
	logger := slog.New(handler)
	// 可选:把全局默认也换成 slog,兼容 slog.Info 调用点。
	slog.SetDefault(logger)

	// 进程启动日志(无 request 域):端口、环境便于对照发布。
	ctx := context.Background()
	slog.InfoContext(ctx, "server starting",
		slog.Int("port", 8080),
		slog.String("env", "production"),
	)

	// 模拟一次下单请求完成。
	handleCreateOrder(logger, "req-1", "o-456")
}

建议运行:

go run .

期望输出(时间戳与 latency 数值会变):

{"time":"...","level":"INFO","msg":"server starting","port":8080,"env":"production"}
{"time":"...","level":"INFO","msg":"order created","request_id":"req-1","route":"POST /orders","order_id":"o-456","latency":...}

结合场景再看三个关注点

  1. JSON Handler 面向检索
    下单日志要能按 order_id / request_id 过滤;字符串拼接不适合生产。

  2. With = 请求域固定字段
    中间件设一次,业务里只补 order_id、错误码等差异字段。

  3. 级别在 Handler 过滤
    LevelInfo 时 Debug 不序列化,热路径少付成本。

核心概念与准确模型

标准库 log 的位置

log.Println("something")
log.Printf("count=%d", n)

特点:全局、无级别 API、弱结构、格式固定。适合 CLI 工具与短生命周期程序。长期服务应迁到 slog 或等价库。

log/slog 模型

概念作用
Logger门面:Info/Warn/Error/Log/With
Handler真正输出:过滤级别、编码、写到 io.Writer
Attr / slog.String类型化字段
Level / LevelVar静态或运行时可变阈值
Record一条日志的时间、级别、消息、字段

内置 Handler:

  • TextHandler:人类可读
  • JSONHandler:机器友好

自定义 Handler 可做:采样、脱敏、冗余字段剥离、桥接到 syslog/OTel。

级别

常见:Debug < Info < Warn < Error

var level slog.LevelVar
level.Set(slog.LevelInfo)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &level})
// 运维接口或信号里:level.Set(slog.LevelDebug)

原则:

  • Info:默认审计级关键业务事实
  • Warn:可恢复异常、降级
  • Error:失败需关注
  • Debug:诊断细节,默认关

避免用 Fatal/panic 当日志级别:log.Fatalos.Exit跳过 defer,破坏优雅关闭。

Context 集成

slog.InfoContext(ctx, "query done", slog.Int64("rows", n))

自定义 Handler 可从 ctx 提取 trace_id/span_id。中间件负责把 request id 放进 context 或 Logger.With。见 中间件可观测性

错误字段

if err != nil {
	logger.Error("save failed", slog.Any("err", err))
	// 或 slog.String("err", err.Error()) 视检索需求
}

包装错误(错误包装)让日志保留链路上下文;但对外响应仍要脱敏。

第三方库速览

特点何时考虑
slog标准库、零额外依赖、生态增长快新项目默认
zap高性能、生态成熟、API 双模式已有 Uber 栈/极致分配控制
zerolog零分配链式 API超高 QPS 且团队接受其风格

桥接:可用适配器把旧 log 或第三方输出接到 slog,逐步迁移。

设计动机

  1. 可查询性
    字段化后才能按 order_id/code 过滤,而不是正则啃字符串。
  2. 关联性
    同一 request_id/trace_id 串起跨服务日志。
  3. 成本可控
    级别与采样避免“全量 Debug”烧盘与烧 CPU。
  4. 与 Go 显式风格一致
    不鼓励隐式全局魔法;但提供 SetDefault 降低改造成本。

边界情况与反直觉行为

1. 默认 Logger 与测试

测试中 SetDefault 会影响进程全局;并行测试需隔离或注入 Logger。

2. 高基数标签

user_id 当指标标签会爆炸;日志字段可以有高基数,但检索与保留策略要设计。指标与日志职责不同。

3. 结构化不是“把整对象塞进去”

slog.Any("req", bigStruct) 可能深反射、循环引用、泄露密码字段。显式白名单字段。

4. 异步日志丢数据

有些封装异步写盘,进程崩溃可能丢尾部日志。关键路径要理解是否同步 flush。

5. 多 Writer

同时写 stdout 与文件时,注意多写失败语义与阻塞;容器环境常只打 stdout,交给采集层。

常见误区

[!warning] 常见误区:字符串拼接当结构 错误:fmt.Sprintf("user=%s order=%s", u, o)
正确:独立 Attr,便于索引与类型一致。

[!warning] 常见误区:密码/Token 进日志 错误:Authorization 头原样打印。
正确:中间件脱敏;Handler 层黑名单。

[!warning] 常见误区:到处 log.Fatal 错误:初始化失败直接 Exit,跳过资源清理。
正确:return errmain,统一退出码与清理。

[!warning] 常见误区:Info 刷屏 错误:每条循环/每字节一条 Info。
正确:聚合、采样、Debug 下沉、指标补量。

[!warning] 常见误区:无关联 ID 错误:全是“save failed”无法对齐请求。
正确:请求入口注入 request_id/trace_id

与相邻概念对比

概念差异
指标 metrics聚合数值;低基数;告警主通道之一
追踪 tracing跨服务跨度与时序;日志用 id 关联
审计 audit合规级不可抵赖记录;常独立管道
fmt 输出给人看的临时打印,不是运维数据面

工程实践

  1. 新项目默认 slog + JSON(生产)/ Text(本地)
  2. Logger 依赖注入:结构体字段或参数传递;请求级 With
  3. 统一字段字典request_idpeererror_code 命名一致。
  4. 错误日志带操作名msg 稳定可聚合,细节放字段。
  5. 动态级别LevelVar + 受控 admin 接口(务必鉴权)。
  6. 与中间件配合:记录 method/path/status/latency,不在每个业务里复制。
  7. 测试:对 Handler 用 bytes.Buffer 断言关键字段存在。
  8. 性能:热路径避免昂贵字段计算;Debug 字段可用惰性或先 Enabled 判断。
if logger.Enabled(ctx, slog.LevelDebug) {
	logger.Debug("payload", slog.String("body", debugDump(b)))
}

可验证实验

实验 1:JSON 输出

运行最小示例,确认一行一个 JSON 对象且含 level/msg

实验 2:级别过滤

Level=InfoDebug 不应出现;改为 Debug 后出现。

实验 3:With 继承

子 Logger 打日志应同时含父字段与新字段。

实验 4:LevelVar

运行中修改 LevelVar,观察后续日志详细度变化。

实验 5:脱敏

写一个包装 Handler,drop/replace password 键,用单测验证。

本节总结

  • 本质:结构化事件流,不是 printf 日记。
  • 标准答案log/slog + 合适 Handler + 请求关联字段。
  • 红线:无密钥、无滥用 Fatal、无无界刷盘。
  • 下一步可观测性 把日志与指标/追踪对齐;配置 管理级别与输出。

自测题

概念题

  1. LoggerHandler 如何分工?
  2. 为什么生产更常选 JSONHandler?
  3. log.Fatal 的主要工程危害是什么?

代码推理题

l := slog.Default().With("k", 1)
l = l.With("k", 2)
l.Info("x")

最终记录里 k 通常以哪个为准?设计上应否重复同名键?

工程思考题

QPS 5 万的服务要把每个请求 body 打 Debug。有什么风险?如何改?

参考答案

展开
  1. Logger 提供调用 API 与字段叠加;Handler 负责级别过滤、编码与输出。
  2. 便于集中采集、索引字段查询、与告警流水线集成。
  3. os.Exit 跳过 defer,资源与在途请求无法优雅收尾。
    代码题:后写字段覆盖/并存行为依赖 Handler 对重复键处理;工程上应避免重复键,保持字段字典干净。
    工程题:体积极大、含 PII、CPU/IO 打满;应默认关闭、采样、截断、哈希、仅失败请求记录、或进专用审计通道。

延伸阅读与资料来源

资料类型支撑
Package log/slog标准库文档Logger/Handler/Level
Structured Logging with slog官方博客设计与用法
Package log标准库文档旧 API 边界
OpenTelemetry Logs concepts外部规范与可观测性对齐(扩展)

笔记元信息

  • 建议文件名:go-logging.md
  • 所属阶段:服务工程
  • 学习顺序:可观测性入口
  • 建议下一篇:Go 可观测性Go 配置管理
  • 本篇状态:已深化(结构完整;以 slog 为默认)
创建于 2026/6/25 更新于 2026/7/15