当前位置: 首页 > news >正文

Go 用 slog 打结构化日志:分级、上下文字段与自定义 Handler 实战

Go 用 slog 打结构化日志:分级、上下文字段与自定义 Handler 实战

线上出了问题,你grep日志想按user_id把一次请求的所有日志串起来,结果发现日志长这样:

2026/08/07 09:12:33 order created for user 8812 amount 199.00 2026/08/07 09:12:33 payment failed: gateway timeout

第二行根本不知道是哪个用户、哪个订单。用fmt.Sprintf拼出来的日志,机器没法解析,人也串不起来。Go 1.21 把log/slog收进标准库,专治这个病。这篇手把手把它用明白。

先看朴素写法的问题

老代码通常是这样:

log.Printf("order created for user %d amount %.2f",userID,amount)

问题有三个:字段名和值混在一句话里,日志系统(Loki、ELK)无法结构化索引;级别只有一个,线上想只看 error 得靠 grep;想给一次请求的所有日志都带上request_id,得每行手动拼。

slog 三分钟上手

slog的核心是 key-value 结构化输出。默认给你一个文本 Handler:

packagemainimport("log/slog""os")funcmain(){// JSONHandler 直接吐 JSON,喂给日志采集器最省事logger:=slog.New(slog.NewJSONHandler(os.Stdout,nil))logger.Info("order created",slog.Int("user_id",8812),slog.Float64("amount",199.00),)}

输出:

{"time":"2026-08-07T09:12:33Z","level":"INFO","msg":"order created","user_id":8812,"amount":199}

现在user_id是独立字段,采集器能直接按它建索引、做聚合。slog.Int/slog.Float64这类强类型辅助函数比裸传"user_id", 8812更快(避免any装箱),高频日志路径上值得用。

也可以设成全局默认 logger,然后直接用包级函数:

slog.SetDefault(logger)slog.Warn("gateway slow","latency_ms",820)// 无需再传 logger

分级:让线上只看你想看的

slog内置 Debug/Info/Warn/Error 四级。生产环境通常只想要 Info 以上,本地调试才开 Debug。通过HandlerOptions.Level控制:

opts:=&slog.HandlerOptions{Level:slog.LevelInfo,// 低于 Info 的直接丢弃,零开销}logger:=slog.New(slog.NewJSONHandler(os.Stdout,opts))logger.Debug("cache hit","key","u:8812")// 不会输出logger.Error("payment failed","err","gateway timeout")

想运行时动态调级别(比如线上临时开 Debug 排查),用slog.LevelVar:

varlvl=new(slog.LevelVar)// 默认 Infolvl.Set(slog.LevelInfo)logger:=slog.New(slog.NewJSONHandler(os.Stdout,&slog.HandlerOptions{Level:lvl}))// 收到信号后热切到 Debug,不用重启进程lvl.Set(slog.LevelDebug)

LevelVar内部用原子操作,并发调Set/Level是安全的。

上下文字段:把一次请求的日志串起来

这才是结构化日志真正省事的地方。用With派生一个带固定字段的子 logger,之后每条日志自动带上:

funchandleOrder(reqIDstring,userIDint){// 派生一次,后面所有日志都带 request_id 和 user_idlog:=slog.With(slog.String("request_id",reqID),slog.Int("user_id",userID),)log.Info("order created","amount",199.00)log.Error("payment failed","err","gateway timeout")}

两行日志都会带上request_id,线上直接按它一 grep 就把整条链路捞出来了。With返回的是新 logger,不改原来的,并发安全。

配合context.Context跨函数传递更自然,用InfoContext系列方法:

typectxKeystruct{}funcwithLogger(ctx context.Context,l*slog.Logger)context.Context{returncontext.WithValue(ctx,ctxKey{},l)}funcloggerFrom(ctx context.Context)*slog.Logger{ifl,ok:=ctx.Value(ctxKey{}).(*slog.Logger);ok{returnl}returnslog.Default()}funcchargeUser(ctx context.Context){// 从 ctx 拿到带 request_id 的 logger,深层函数也能续上同一条链路loggerFrom(ctx).InfoContext(ctx,"charging")}

分组:给字段加命名空间

字段多了容易撞名(两个子系统都叫id)。用WithGroupslog.Group加前缀:

logger.Info("request done",slog.Group("http",slog.String("method","POST"),slog.Int("status",200),),)// JSON: "http":{"method":"POST","status":200}

自定义 Handler:脱敏与字段改写

真实项目里常有两个需求:密码/token 不能进日志,时间字段要换格式。用HandlerOptions.ReplaceAttr在写出前拦截每个字段:

opts:=&slog.HandlerOptions{ReplaceAttr:func(groups[]string,a slog.Attr)slog.Attr{// 把 password 字段的值统一抹掉ifa.Key=="password"{returnslog.String("password","***")}// 把内置 time 字段换成 Unix 秒ifa.Key==slog.TimeKey&&len(groups)==0{returnslog.Int64("ts",a.Value.Time().Unix())}returna},}logger:=slog.New(slog.NewJSONHandler(os.Stdout,opts))logger.Info("login","user","amy","password","hunter2")// {"ts":1754557953,"level":"INFO","msg":"login","user":"amy","password":"***"}

ReplaceAttr每个字段都会走一遍,高频路径上别写太重的逻辑。返回一个Key为空的Attr可以彻底丢弃该字段。

一个容易踩的坑:奇数个参数

slog支持logger.Info("msg", "key", val)这种松散写法,但如果 key-value 个数对不上(漏了一个值),它不会 panic,而是把落单的那个当成一个特殊的!BADKEY字段:

logger.Info("oops","user_id")// 少了值// ...,"!BADKEY":"user_id"} —— 静默出错,不易发现

高频或关键日志建议一律用slog.Int/slog.String这类强类型形式,既快又不会漏配对。可以用go vet(启用sloglint或第三方 linter)在 CI 里把松散写法的配对问题挡住。

小结

  • slog是 Go 1.21+ 标准库,输出结构化 key-value,机器可索引、人可串链路,取代log.Printf拼字符串。
  • JSONHandler喂日志采集器;HandlerOptions.LevelLevelVar运行时热调级别,不用重启。
  • With/slog.With派生带固定字段的子 logger,把request_id一次带上,是串联一次请求所有日志的关键。
  • ReplaceAttr脱敏和字段改写;强类型slog.Int等既快又避免!BADKEY静默配对错误。
  • 记忆点:别再 Printf 拼日志了——把值当字段传,让机器帮你串链路。
http://www.jsqmd.com/news/1347144/

相关文章:

  • node-tesseract实战:从图片到文本的简单转换技巧
  • FastANI完整指南:5步掌握微生物基因组相似性快速分析
  • 深圳抖音代运营如何选择?以中网创信为例的避坑指南 2026年8月7日15分更新 - 中国远见品牌企业资讯
  • DeepSeek V4长上下文推理与NVIDIA Blackwell架构:软硬协同下的AI部署实战
  • ComfyUI-Manager终极指南:如何快速安装和管理AI工作流插件
  • 终极本地化网盘直链下载助手:九大平台一键高速下载解决方案
  • 基于DenseRMA_ViT与注意力机制的轻量级水果识别系统嵌入式部署实践
  • 深度解析OCRmyPDF:企业级文档数字化的5大技术优势
  • MyBatis-Plus更新操作深度解析:从updateById到条件构造器的实战指南
  • 3步掌握Intel RealSense相机标定:从新手到专家的完整指南
  • 宿州抖音公会演出经纪许可证代办服务商哪家好 - 品牌品鉴馆
  • 5分钟上手Mermaid Live Editor:让图表创作像写笔记一样简单
  • 基于YOLOv8+pyqgt5的道路坑洼检测系统123设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 抚顺甲醛检测多少钱一次?2026新房入住前,先把点位、项目和报告用途说清楚 - CMA甲醛检测
  • React useId 实战:治好 SSR 水合警告、表单无障碍与唯一 ID 生成
  • JMeter从零入门:Java环境配置、安装启动与性能测试实战指南
  • Windows驱动签名永久禁用:原理、风险与bcdedit实操指南
  • 终极指南:如何用Wand-Enhancer免费解锁WeMod专业版完整功能
  • 从0到1开发goa/goap自定义Action:以ChopTreeAction为例的完整教程
  • 告别繁琐剪辑:5分钟掌握AI视频生成神器MoneyPrinterTurbo完整指南
  • 天赐范式第127天:基座结构的实例回响——三根支柱在86-126天中的八次现身
  • MoocDownloader深度解析:从在线学习到离线掌控的完整指南
  • 5个步骤掌握PlaceholderAPI:让Minecraft服务器内容真正“活“起来
  • 10分钟免费掌握Path of Building:流放之路终极Build规划工具完整指南
  • 厦门招聘平台哪个好:【帅聘网】口碑优良 - 晴光转树
  • MySQL运维利器pt-kill:精准拦截问题SQL实战
  • hwinfo:如何用5行代码解决跨平台硬件信息采集难题?[特殊字符]
  • 3步掌握Hackintool:黑苹果配置从困惑到精通的探索之旅
  • C++函数指针、函数指针数组与成员函数指针:原理、应用与性能优化
  • 3分钟搭建AI数据标注平台:Label Studio让多模态数据处理变得简单