Go log/slog.NewTextHandler 怎样输出纯文本结构化日志
来源:17golang原创
时间:2026-11-01 14:49:03 257浏览 收藏
如果 Go 服务已经使用 log/slog,但默认日志格式不够稳定、终端排障时字段不够直观,可以把 Handler 换成 slog.NewTextHandler。它会把一条结构化记录整理为便于阅读和解析的 key=value 文本,例如 level=INFO msg=started count=3。
官方资料:https://pkg.go.dev/log/slog#NewTextHandler
本文只解决一个问题:怎样用 NewTextHandler 选好输出目标、最低日志级别和字段策略。需要机器直接消费 JSON 时,应改用 slog.NewJSONHandler,不要把文本格式硬塞进日志采集链路。

先把结构化记录写成文本
NewTextHandler 接收两个参数:一个是实现了 io.Writer 的输出目标,另一个是可选的 *slog.HandlerOptions。创建 Handler 后,再交给 slog.New 得到 Logger。
package main
import (
"log/slog"
"os"
)
func main() {
// 将文本日志写到标准错误,适合容器日志和本地终端排障。
handler := slog.NewTextHandler(os.Stderr, nil)
logger := slog.New(handler)
// 结构化字段会以 key=value 形式出现在同一条日志中。
logger.Info("worker started", "queue", "orders", "count", 3)
}
这段配置的关键不是“打印一行字符串”,而是保留了消息、级别和属性字段的结构。文本 Handler 会生成类似下面的结果:
time=2026-11-01T14:00:00+08:00 level=INFO msg="worker started" queue=orders count=3
实际时间会由运行时记录决定;排查文章中的输出只用于展示字段形状,不代表某一次本机运行结果。
输出到文件时,先明确 Writer 的生命周期
想把文本日志写入文件时,打开文件、处理错误,并让文件在进程退出前关闭。NewTextHandler 不负责替你管理 Writer,也不会替你创建目录。
package main
import (
"log/slog"
"os"
)
func main() {
// 创建或覆盖示例日志文件;生产环境通常要接入轮转策略。
file, err := os.OpenFile("service.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
panic(err)
}
// 关闭动作放在创建成功之后,避免对空文件句柄调用 Close。
defer file.Close()
// Writer 的写入失败会由 Handler 在处理记录时返回给内部调用链。
logger := slog.New(slog.NewTextHandler(file, nil))
logger.Info("file logging enabled", "component", "scheduler")
}
如果服务运行在容器中,通常直接写 os.Stderr 更容易交给平台采集;如果必须写文件,还要把权限、轮转、磁盘满和多进程竞争纳入运维设计。
用 HandlerOptions 控制最低级别和来源
默认最低级别是 INFO。当 HandlerOptions 的 Level 设为 slog.LevelDebug 时,Debug 记录才会进入输出;AddSource 可以附加调用位置,但它会增加每条记录的字段长度。
package main
import (
"log/slog"
"os"
)
func newLogger(debug bool) *slog.Logger {
level := slog.LevelInfo
if debug {
// 只在排障开关打开时放行 Debug,避免生产日志量失控。
level = slog.LevelDebug
}
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
AddSource: debug,
})
return slog.New(handler)
}
func main() {
logger := newLogger(true)
logger.Debug("cache inspection", "key", "orders:1")
logger.Info("request accepted", "status", 202)
}
这里的判断顺序很重要:先决定当前环境的最低级别,再创建 Handler。若日志没有输出,先确认记录级别是否低于 HandlerOptions.Level,不要先怀疑文本格式化失败。

需要全局生效时设置默认 Logger
只创建一个局部 Logger,并不会改变 slog.Info 等顶层函数使用的默认 Handler。需要让应用中现有的顶层调用也输出为文本时,可以显式调用 slog.SetDefault。
package main
import (
"log"
"log/slog"
"os"
)
func main() {
// 统一默认输出,让 slog 顶层函数使用同一个文本 Handler。
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
slog.SetDefault(logger)
// 旧 log 包的标准输出也会接入默认 slog Handler。
slog.Info("new logger ready", "mode", "text")
log.Print("legacy call")
}
迁移旧代码时,这个入口很有用,但仍要留意旧日志的参数语义与字段命名。不要在多个初始化路径反复调用 SetDefault,否则排障时很难判断最终生效的是哪个 Writer。
用 With、WithGroup 和 ReplaceAttr 整理字段
服务日志通常有一批公共字段,例如服务名、环境和请求 ID。用 With 绑定公共字段,再用 WithGroup 给子系统字段加前缀,能减少重复和同名覆盖。
package main
import (
"log/slog"
"os"
)
func main() {
// 公共字段只绑定一次,后续每条日志都会带上 service 和 env。
base := slog.New(slog.NewTextHandler(os.Stderr, nil)).With(
"service", "billing",
"env", "prod",
)
worker := base.WithGroup("worker")
// 分组后的字段会显示为 worker.id、worker.queue,降低重名歧义。
worker.Info("job accepted", "id", "job-17", "queue", "invoice")
}
若需要删除时间、缩短源码路径或做敏感字段改写,可以在 HandlerOptions.ReplaceAttr 中返回空属性或替换后的属性。字段清理应该集中在 Handler 层完成,避免每个业务调用点各写一套脱敏逻辑。
package main
import (
"log/slog"
"os"
)
func main() {
replace := func(groups []string, attr slog.Attr) slog.Attr {
// 去掉顶层时间字段,让采集平台统一补时间。
if attr.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
// 不把 token 原文写入日志;真实项目应覆盖更多敏感键。
if attr.Key == "token" {
return slog.String("token", "[redacted]")
}
return attr
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
ReplaceAttr: replace,
}))
logger.Warn("credential refreshed", "token", "secret-value")
}
按症状定位配置问题
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 完全没有输出 | Writer 是否有效、初始化是否执行 | 先换成 os.Stderr,再检查打开文件的错误 |
| Info 有、Debug 没有 | HandlerOptions.Level | 需要调试时使用 slog.LevelDebug 或 LevelVar |
| 字段同名难以区分 | 业务 Logger 是否缺少分组 | 用 WithGroup 给子系统字段加前缀 |
| 日志里出现敏感值 | 是否集中配置 ReplaceAttr | 在 Handler 层做脱敏,不依赖调用者自觉 |
| 采集器要求 JSON | 输出协议而非可读性 | 改用 NewJSONHandler,不要正则解析文本日志 |
结论
slog.NewTextHandler(writer, opts) 的最小可用组合是:先选定 Writer,再用 HandlerOptions 决定最低级别、source 字段和属性改写,最后交给 slog.New。本地排障适合输出到 os.Stderr 并放开 Debug;生产环境更应该固定字段策略、限制日志级别,并根据采集协议决定使用 TextHandler 还是 JSONHandler。
常见追问
NewTextHandler 和 NewJSONHandler 怎样选?
给人直接看、需要保留 key=value 可读性时选 TextHandler;下游是日志平台或程序解析时选 JSONHandler。
为什么设置了 TextHandler 但 slog.Info 还是旧格式?
通常只创建了局部 Logger,没有调用 slog.SetDefault。检查调用点使用的是自定义 Logger 还是顶层函数。
HandlerOptions.Level 能动态调整吗?
可以把 Level 设置为 *slog.LevelVar,再通过 Set 改变最低级别;修改时仍应限制权限和变更范围。
-
336 收藏
-
301 收藏
-
331 收藏
-
275 收藏
-
380 收藏
-
250 收藏
-
206 收藏
-
345 收藏
-
Golang · Go教程 | 34分钟前 | 错误处理 · go · encoding/json · Go errors.Is errors.As Unwrap json.MarshalerError430 收藏
-
366 收藏
-
212 收藏
-
424 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习