Go Slog 实战指南:标准库结构化日志从入门到生产配置
Go log/slog 中文实战指南:默认 logger 与四个级别、TextHandler/JSONHandler、结构化属性与分组、With 链式上下文、HandlerOptions 定制(AddSource/ReplaceAttr/LevelVar 动态级别),以及如何用观测云 DataKit 采集 Go 日志并配置告警。
Slog 是 Go 1.21 起内置在标准库中的结构化日志包(log/slog),它把"高性能 + 结构化 + 前后端分离(Handler 可替换)"这三件事第一次同时带进了标准库。本文从默认用法讲到生产配置,帮助你把 Go 项目的日志体系一次性搭对。
核心要点速览
- Slog 是 Go 标准库的结构化日志方案:
log/slog提供 Debug/Info/Warn/Error 四个级别,默认输出带时间戳和级别的文本。 - 两种内置 Handler:
TextHandler输出 logfmt 风格键值对适合开发,JSONHandler输出 JSON 适合生产采集。 - 结构化靠属性(Attr):
slog.String("key", v)、logger.With()绑定常驻字段、slog.Group()分组嵌套。 - 生产落地:JSONHandler 写 stdout,观测云 DataKit 自动采集解析,监控器对 error 级日志告警。
快速上手:默认 logger 与日志级别
Slog 提供一个开箱即用的默认 logger,直接调包级函数即可:
package main
import "log/slog"
func main() {
slog.Debug("调试细节") // 默认不输出(默认级别 INFO)
slog.Info("服务启动", "port", 8080)
slog.Warn("配置项缺失,使用默认值")
slog.Error("数据库连接失败", "err", "connection refused")
}
输出:
2026/08/25 14:30:22 INFO 服务启动 port=8080
2026/08/25 14:30:22 WARN 配置项缺失,使用默认值
2026/08/25 14:30:22 ERROR 数据库连接失败 err="connection refused"
四个级别对应数值:Debug=-4、Info=0、Warn=4、Error=8。数值设计留下了自定义中间级别的空间(如 2 表示 Info+2 的 Notice)。
如何用 Handler 控制输出格式与目的地?
Slog 的设计精髓是前端 API 与后端 Handler 分离:业务代码只调 slog API,输出格式和目的地由 Handler 决定。
TextHandler:开发环境的人类可读格式
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("用户登录", "user", "zhangsan", "ip", "10.0.0.8")
输出 logfmt 风格键值对:
time=2026-08-25T14:31:05.123+08:00 level=INFO msg=用户登录 user=zhangsan ip=10.0.0.8
JSONHandler:生产环境的结构化格式
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("用户登录", "user", "zhangsan", "ip", "10.0.0.8")
{"time":"2026-08-25T14:31:05.123456+08:00","level":"INFO","msg":"用户登录","user":"zhangsan","ip":"10.0.0.8"}
JSON 输出是接入日志平台的标准姿势——观测云 DataKit 采集后自动解析为字段,无需任何正则。
结构化三板斧:属性、With 与分组
强类型属性
消息后面跟键值对,推荐使用强类型的 Attr 构造器(性能更好、类型安全):
logger.Info("订单创建成功",
slog.String("order_id", "A-1024"),
slog.Int("amount", 9900),
slog.Duration("elapsed", 35*time.Millisecond),
)
注意避免 logger.Info("msg", "key", value) 松散写法的键值不配对错乱——编译器不检查,配错就静默产生 !BADKEY 字段。用 slog.String() 等强类型形式可根治。
With:给 logger 绑定常驻字段
同一请求/模块的多条日志共享的字段,用 With 绑定一次:
reqLogger := logger.With(slog.String("request_id", rid), slog.String("user", "zhangsan"))
reqLogger.Info("开始处理订单")
reqLogger.Info("订单处理完成") // 两条日志都自动带 request_id 和 user
Group:字段分组嵌套
logger.Info("支付回调",
slog.Group("payload",
slog.String("order_id", "A-1024"),
slog.Int("amount", 9900),
),
)
JSON 输出中 payload 会成为嵌套对象,字段结构更清晰。
生产配置:HandlerOptions 详解
opts := &slog.HandlerOptions{
Level: slog.LevelInfo, // 最低输出级别
AddSource: true, // 附加源码位置(文件:行号)
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 例如把 level 字段值转小写,统一风格
if a.Key == slog.LevelKey {
a.Value = slog.StringValue(strings.ToLower(a.Value.String()))
}
return a
},
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
- Level 接受
slog.LevelVar,运行期可动态调整——配合 HTTP 管理接口即可实现不重启调级别; - AddSource 排障期很有用,但有性能开销,生产可关;
- ReplaceAttr 是统一字段命名规范(如 time→@timestamp、level→status)的官方扩展点。
最后用 slog.SetDefault(logger) 让包级函数和 log 标准库的存量调用(通过 slog.NewLogLogger 桥接)都走新配置。
错误日志怎么记?
Go 的错误是值,直接作为属性记录:
if err := processPayment(order); err != nil {
logger.Error("支付处理失败",
slog.String("order_id", order.ID),
slog.Any("error", err),
)
}
想让自定义错误类型输出更丰富的结构,可以实现 slog.LogValuer 接口,把错误码、原因链等作为分组字段输出。Slog 不自动带堆栈——需要堆栈时用 runtime/debug.Stack() 或借助包装库(如 pkg/errors)附加上下文。
观测云落地:Go 服务日志采集与分析
- 应用侧:生产环境固定 JSONHandler + stdout 输出(容器最佳实践);字段命名遵循统一契约(time/level/msg/error)。
- DataKit 采集:容器环境自动采集 stdout;主机部署在
conf.d/log/logging.conf配置文件采集,source设为go-app、service设为服务名。JSON 自动解析为字段。 - Pipeline 标准化:把
level映射为观测云标准status字段(error →error,warn →warning),time解析为事件时间,保证查看器和告警按统一维度工作。 - 检索分析:日志查看器按
service、status、request_id 过滤;聚类分析自动归纳错误模式;图表模式统计错误率趋势。 - 告警:监控 > 监控器 > 新建日志检测监控器,对
status:error日志设阈值,告警策略绑定钉钉/企业微信/飞书通知对象。 - 链路关联:接入 OpenTelemetry 后,用
slog的 context 方法(见下一篇)把 trace_id 注入日志,实现与观测云 APM 的双向跳转。 - 成本控制:按级别拆索引——error 走标准索引长保留,info 走低频索引短保留,历史数据转发归档对象存储。
常见问题(FAQ)
Go 1.21 之前的版本能用 Slog 吗?
可以,使用 golang.org/x/exp/slog 实验包,API 与标准库一致。升级到 Go 1.21+ 后把 import 换成 log/slog 即可。新项目直接用标准库版本。
Slog 的性能比得上 Zerolog/Zap 吗?
Slog 明显快于 Logrus 等老库,但与 Zerolog(零分配)和 Zap 仍有差距。绝大多数业务系统感知不到差异;只有确认日志是性能瓶颈的极高吞吐服务,才需要考虑 Zerolog/Zap——而且可以用它们作为 Slog 的 Handler 后端,业务代码不变。
Slog 支持日志文件轮转吗?
标准库不含轮转。两种推荐做法:容器化部署写 stdout,轮转交给容器运行时;主机部署配合系统 logrotate(copytruncate 模式),或使用 lumberjack 等第三方 writer 作为 io.Writer 传给 Handler。
已有大量 log.Printf 的老项目怎么迁移?
分步走:先 slog.SetDefault + slog.NewLogLogger(handler, slog.LevelInfo) 把标准库 log 输出桥接到 slog Handler,存量代码不用动;新代码逐步用 slog API,老代码按模块渐进替换。
系列阅读
- 上一篇:FastAPI 日志实战
- 下一篇:Go 上下文日志实战
- 相关阅读:Zerolog 实战 | Zap 实战 | 日志级别详解