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

Go slog.HandlerOptions AddSource 怎样输出调用位置

来源:17golang原创

时间:2026-10-22 21:45:04 402浏览 收藏

我在给 Go 服务补结构化日志时,最先想要的不是更多字段,而是“这条日志到底从哪一行打出来”。slog.HandlerOptions 的 AddSource 就是为这个问题准备的:设置为 true 后,内置 Handler 会把日志调用的源文件和行号加入输出。官方资料入口:https://pkg.go.dev/log/slog#HandlerOptions。

AddSource 只负责让 Handler 生成 source 信息;TextHandler 会把它写成 文件:行号,JSONHandler 会写成结构化的 Source 记录。若日志函数再包了一层,调用位置可能变成包装函数,需要自己取得调用者的程序计数器。

AddSource 到底记录了什么

这个选项不是给日志消息自动追加一段字符串,而是让日志记录保留程序计数器,Handler 再根据它解析出调用位置。直接调用 logger.Info 时,通常能看到业务代码所在文件和行号;当调用经过自定义包装函数时,slog 反射调用栈得到的可能是包装函数那一行。

TextHandler 的结果适合人读,source 通常呈现为 source=service.go:42。JSONHandler 则把 source 作为结构化对象输出,便于采集端按 file、line 等字段检索。两者的共同点是:只有 Handler 开启 AddSource 且记录中存在源信息时,source 才会出现。

Logger、Record.PC、TextHandler、JSONHandler 与 source 属性的静态关系说明图
图1:结构说明图,查看 AddSource 记录调用位置并交给不同 Handler 输出的关系。

最小配置应该怎么写

先把 Handler 的输出目标和 AddSource 一起配置,再把 Handler 交给 Logger。这里用标准输出演示,代码中的中文注释只解释关键配置,不代表某次本机运行截图。

package main

import (
    "log/slog"
    "os"
)

func main() {
    handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        // 打开调用位置,TextHandler 会输出 source=文件:行号。
        AddSource: true,
    })
    logger := slog.New(handler)

    // 这条日志的调用位置由 slog 记录并交给 Handler 格式化。
    logger.Info("开始处理订单", "order_id", 1001)
}

示例输出可以理解为下面这种形态,文件名和行号会随调用位置变化:

level=INFO source=service.go:18 msg="开始处理订单" order_id=1001

如果改用 slog.NewJSONHandler,字段仍然是结构化日志的一部分,不应再把整行文本拆分后猜测位置。实际项目应根据采集系统需要选择格式,而不是为了看到行号同时维护两套日志字符串。

包装 Logger 后 source 为什么会变化

这是最容易误判的地方。比如业务代码调用 Infof,而 Infof 内部再调用 logger.Info,slog 看到的直接日志调用点就是包装文件。官方包装示例的做法是先检查级别,再用 runtime.Callers 跳过 Callers 和包装函数本身,最后把得到的 PC 传给 slog.NewRecord。

func Infof(logger *slog.Logger, format string, args ...any) {
    // 被过滤的日志不需要计算格式化结果或继续取调用栈。
    if !logger.Enabled(context.Background(), slog.LevelInfo) {
        return
    }

    var pcs [1]uintptr
    // 跳过 runtime.Callers 和 Infof,保留 Infof 的调用者位置。
    runtime.Callers(2, pcs[:])
    record := slog.NewRecord(
        time.Now(),
        slog.LevelInfo,
        fmt.Sprintf(format, args...),
        pcs[0],
    )
    // 让原 Logger 的 Handler 继续负责 source 和最终格式化。
    _ = logger.Handler().Handle(context.Background(), record)
}

包装函数还要考虑参数计算成本和级别过滤。直接使用 logger.Info 的简单场景不必为了“看起来更底层”自己重建 Record;只有确实需要统一格式、保留业务调用者位置时,才值得采用这种包装方式。

业务函数、包装函数、Logger.Log、AddSource 与 source.File/source.Line 的静态关系说明图
图2:结构说明图,查看 Logger 包装与 source.File、source.Line 处理边界。

ReplaceAttr 和生产环境怎么取舍

ReplaceAttr 可以改写或删除内置的 source 属性。常见做法是只保留文件名,去掉机器上的绝对目录;也可以在某些采集链路已经提供调用位置时删除 source,避免重复字段。处理 source 时要先确认值的类型是 *slog.Source,不要把普通业务属性误当成内置字段。

开发排障、灰度发布和错误日志通常值得开启 AddSource;高吞吐的常规信息日志则要结合采样、级别和采集成本评估。它会带来调用栈定位与格式化开销,但是否值得不是固定结论,应该用实际日志量和性能剖析决定。

我会用下面这张清单收尾:

  • Handler 是否真的使用了 HandlerOptions{AddSource: true}。
  • 输出端需要 TextHandler 的文本字段,还是 JSONHandler 的结构化 source。
  • 是否存在包装函数;若存在,是否需要用 runtime.Callers 修正调用者位置。
  • ReplaceAttr 是否会清洗路径、改名或删除 source。
  • 生产环境是否有采样、级别过滤和日志成本控制。

相关问题

AddSource 为什么没有输出 source?

先看创建 Handler 时传入的 options 是否为 nil 或仍是默认值,再确认使用的 Handler 是 TextHandler/JSONHandler 等会处理 source 的实现,并检查 Record 是否带有有效的程序计数器。

能不能只输出文件名不输出完整路径?

可以在 ReplaceAttr 中识别 slog.SourceKey,复制或修改 Source 后只保留文件名。不要直接修改可能被其他处理逻辑共享的对象,包装自定义 Handler 时也要遵守 Record 的复制边界。

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