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

Go slog LogValuer 怎样延迟构造昂贵日志字段

来源:17golang原创

时间:2026-11-13 23:03:56 261浏览 收藏

实现 slog.LogValuer,把昂贵字段计算放进 LogValue() slog.Value,调用日志时传入这个轻量对象,而不是直接传入计算结果。使用内置 JSONHandler 或 TextHandler 时,被日志级别过滤的记录不会进入字段处理,因而可以避免这部分计算。包装对象本身的构造仍然会发生。

官方文档:https://pkg.go.dev/log/slog#LogValuer

我会先从“排查时需要什么”决定日志字段,再考虑怎样计算。比如只需要载荷长度和摘要,就没有必要输出整个请求体。下面用原创示例说明这一取舍;它不是线上事故复盘,也没有声称做过实际性能测量。

任务:关闭调试日志时别先算好字段

常见问题是 Debug 已关闭,CPU 却仍在构造长字符串或扫描大对象。原因往往在日志实参:Go 在进入日志方法之前就会求值,日志级别无法撤回已经发生的函数调用。

// eagerDigest 是项目中假设存在的昂贵计算函数。
// 直接调用它作为实参时,即使 Debug 被过滤,函数也已求值。
logger.Debug("检查载荷", "digest", eagerDigest(payload))

// 包装类型必须轻量;不要在 summary 的构造中提前完成同样计算。
logger.Debug("检查载荷", slog.Any("payload", summary{data: payload}))

这段用于对比实参写法,eagerDigest 是示意函数。真正要推迟的是扫描、编码等工作,不能只是给已经计算好的结果再套一层 LogValuer。若包装构造函数先复制数兆数据,复制也不会因为日志关闭而自动消失。

拆解:级别过滤与值解析各管什么

Logger 先询问 Handler 是否接受当前级别的记录;字段进入处理器后,LogValuer 提供自己的日志表示。对内置处理器而言,级别门槛与值解析配合形成了延迟计算的使用路径。自定义 Handler 则需要正确处理 Value 的解析语义,不能假定所有第三方处理器行为完全一样。

LogValue 可以返回单个值,也可以返回 GroupValue,把一项业务对象展开为可检索的子字段。我的建议是让字段名保持稳定,避免把多个诊断信息全部拼成难以搜索的字符串。

Logger、Handler级别、LogValuer与日志值的依赖关系
原创静态结构图:级别接受条件与字段表示各自归属不同组件。连线不表示执行时间或测量结果。

实现:把计算放进 LogValue

package main

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "log/slog"
    "os"
)

// summary 只保存已有数据的引用,不在构造时计算摘要。
type summary struct {
    data []byte
}

func (s summary) LogValue() slog.Value {
    // 只有日志值被解析时,才扫描数据并构造摘要字符串。
    sum := sha256.Sum256(s.data)
    return slog.GroupValue(
        slog.Int("bytes", len(s.data)),
        slog.String("digest", hex.EncodeToString(sum[:])),
    )
}

func main() {
    level := new(slog.LevelVar)
    level.Set(slog.LevelInfo)
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: level,
    }))
    ctx := context.Background()
    payload := []byte("sample-only")

    // Info 门槛下,这条 Debug 不会让内置处理器解析 summary。
    logger.DebugContext(ctx, "检查载荷", slog.Any("payload", summary{data: payload}))

    // 打开 Debug 后,处理器解析字段并调用 LogValue。
    level.Set(slog.LevelDebug)
    logger.DebugContext(ctx, "检查载荷", slog.Any("payload", summary{data: payload}))
}

示例使用 LevelVar 切换门槛。第一条 Debug 被内置处理器的 Info 门槛过滤;第二条被接受,字段表示才需要计算摘要。预期可以看到第二条日志中的 payload 分组,包括 bytes 和 digest;本文不展示伪造的控制台输出。

这里选择 SHA-256 是为了示范一个需要扫描输入的数据派生操作,不是建议所有日志都计算摘要。小字符串没有必要为追求惰性而增加过多类型。真正昂贵的聚合、编码或对象遍历,才更值得使用这样的接口。

可读性与脱敏:只输出需要的字段

延迟计算解决“什么时候做”,脱敏解决“允许输出什么”。它们可以放在同一个 LogValue 中实现,却不能相互替代。只把输出推迟,并不能防止日志级别打开后泄漏原文。

// 只把允许公开的字段放入日志表示,不输出原始凭据。
type authView struct {
    UserLabel string
}

func (a authView) LogValue() slog.Value {
    return slog.GroupValue(
        slog.String("user", a.UserLabel),
        slog.Bool("credential_hidden", true),
    )
}

这段是独立的类型示例,只公开允许记录的标签和隐藏凭据的状态,不保存或输出真实凭据。字段本身也应服从数据最小化原则:用户标签如果仍包含个人信息,就需要根据实际业务进一步处理。

摘要也不等于脱敏保证。低熵输入的摘要可能被枚举匹配,所以敏感信息应优先不采集、不输出,而不是统一哈希后就认定安全。我的取舍是先定义允许出现在日志里的视图,再在这个视图内决定是否延迟某项派生计算。

业务数据、日志视图、允许字段与隐藏凭据的静态组成
原创静态结构图:日志视图只包含获准表示。延迟计算和字段脱敏是两类职责,不代表安全认证。

性能与边界:延迟不等于缓存

LogValuer 不承诺只执行一次,也不是结果缓存。自定义处理器、多个输出目的地或不同的属性绑定方式,都可能改变解析时机和次数。不要把扣库存、更新状态或发网络请求这样的业务副作用放进 LogValue;让它可重复、可预测。

示例的 summary 保存切片引用,没有复制底层数据。如果数据在字段解析前被修改,日志看到的可能不再是调用方原本想记录的内容;并发改写还可能引入数据竞争。异步处理器更需要明确快照与所有权。需要复制时,应承认复制成本,并把它纳入设计,而不是把延迟计算描述成零成本。

通过 logger.With 预绑定属性时,处理器可能提前处理这些属性,因此不要将请求相关的大对象随意绑定到长期 Logger 上。需要显式控制整段日志准备工作时,也可以先判断 logger.Enabled(ctx, level),再构造字段;这与 LogValuer 是互补工具。

如果自己实现 Handler,应参考 Value.Resolve 的规则,而不是无条件直接调用一次 LogValue。它能够处理返回值仍实现 LogValuer 的情况,并对反复解析提供限制。不要为了追求惰性返回自身形成递归。

需求适合的选择
推迟某一字段计算轻量对象实现 LogValuer
跳过整组昂贵准备Enabled 条件判断
隐藏敏感字段受控日志视图和明确输出白名单
记录调用时状态明确快照与共享数据策略
只计算一次另行设计缓存语义,不能依赖 LogValuer 次数

相关问题

直接传一个 func 就会延迟执行吗?

不会自动获得 LogValuer 语义。需要由实现 LogValue 的类型明确说明怎样产生日志值,不能期待任意函数值都被当作延迟回调。

LogAttrs 能替代 LogValuer 吗?

不能。LogAttrs 用类型化 Attr 表达字段,实参仍遵循 Go 的求值规则;昂贵字段的延迟表示仍可用 LogValuer。

字段被脱敏后还需要日志级别控制吗?

需要。字段内容控制、记录级别和计算开销是不同问题,应分别决定。

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