登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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,不要把文本格式硬塞进日志采集链路。

Go slog TextHandler 从 Logger 经过 HandlerOptions 写入标准错误或文件的结构关系图
图1:NewTextHandler 将结构化记录整理为 key=value 文本并写入目标 Writer 的静态结构图。

先把结构化记录写成文本

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,不要先怀疑文本格式化失败。

Go slog HandlerOptions 过滤日志级别并整理 source 与属性字段的关系图
图2:HandlerOptions 对日志级别、source 字段和属性改写的静态关系图。

需要全局生效时设置默认 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 改变最低级别;修改时仍应限制权限和变更范围。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>