You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优雅实现支持嵌套步骤递增的Go slog Handler

实现支持层级步骤的Go slog Handler(单线程场景)

需求概述

要实现一个slog Handler,自动递增并记录步骤编号,支持用户手动管理多嵌套层级,仅需支持单线程使用。预期效果是:用户通过根Handler控制层级,所有衍生的Logger实例(通过With/WithGroup生成)都共享同一套步骤状态,日志输出按层级生成如STEP 1)、STEP 2.1)格式的编号。

当前问题

原有设计中,调用logger.With/logger.WithGroup会生成新的SteppedHandler实例,用户无法直接获取这些实例来调用PushLevel/PopLevel;若通过共享steps切片指针实现状态同步,又会导致所有Handler实例的状态被同时修改,虽然功能上能满足,但不符合用户对“仅通过根Handler控制层级”的优雅使用需求。

优雅解决方案

核心思路

将步骤层级的管理逻辑抽离到独立的StepManager结构体中,所有SteppedHandler实例(包括原实例和衍生实例)共享同一个StepManager指针。用户仅需持有根SteppedHandler,通过其暴露的PushLevel/PopLevel方法控制层级,所有Logger实例都会自动使用最新的步骤状态生成编号。

代码实现

1. 步骤管理器结构体

import (
    "context"
    "strings"
    "strconv"
    "log/slog"
    "os"
)

type StepManager struct {
    levels []int // 存储各层级的当前计数,如[2,3]对应STEP 2.3
}

func NewStepManager() *StepManager {
    return &StepManager{levels: []int{0}} // 初始层级计数为0,首次日志自动递增到1
}

// PushLevel 新增一个嵌套层级
func (sm *StepManager) PushLevel() {
    sm.levels = append(sm.levels, 0)
}

// PopLevel 退出当前嵌套层级(最少保留一个层级)
func (sm *StepManager) PopLevel() {
    if len(sm.levels) > 1 {
        sm.levels = sm.levels[:len(sm.levels)-1]
    }
}

// NextStep 生成下一个步骤编号字符串,并递增当前层级计数
func (sm *StepManager) NextStep() string {
    sm.levels[len(sm.levels)-1]++
    
    var buf strings.Builder
    buf.WriteString("STEP ")
    for idx, num := range sm.levels {
        if idx > 0 {
            buf.WriteByte('.')
        }
        buf.WriteString(strconv.Itoa(num))
    }
    buf.WriteString(")")
    return buf.String()
}

2. 实现SteppedHandler

type SteppedHandler struct {
    h       slog.Handler    // 底层Handler,负责实际日志输出
    manager *StepManager    // 共享的步骤管理器
}

func NewSteppedHandler(baseHandler slog.Handler) *SteppedHandler {
    return &SteppedHandler{
        h:       baseHandler,
        manager: NewStepManager(),
    }
}

// PushLevel 暴露步骤管理器的Push方法,用户通过根Handler调用
func (sh *SteppedHandler) PushLevel() {
    sh.manager.PushLevel()
}

// PopLevel 暴露步骤管理器的Pop方法,用户通过根Handler调用
func (sh *SteppedHandler) PopLevel() {
    sh.manager.PopLevel()
}

// WithAttrs 实现slog.Handler接口,返回新的Handler实例并共享步骤管理器
func (sh *SteppedHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &SteppedHandler{
        h:       sh.h.WithAttrs(attrs),
        manager: sh.manager,
    }
}

// WithGroup 实现slog.Handler接口,返回新的Handler实例并共享步骤管理器
func (sh *SteppedHandler) WithGroup(name string) slog.Handler {
    return &SteppedHandler{
        h:       sh.h.WithGroup(name),
        manager: sh.manager,
    }
}

// Handle 实现slog.Handler接口,添加步骤编号后转发给底层Handler
func (sh *SteppedHandler) Handle(ctx context.Context, r slog.Record) error {
    // 方式1:将步骤编号作为属性添加到日志
    r.AddAttrs(slog.String("step", sh.manager.NextStep()))
    // 方式2:直接修改日志消息(若需要将编号前置到消息中)
    // r.Message = sh.manager.NextStep() + " " + r.Message
    return sh.h.Handle(ctx, r)
}

3. 使用示例

func main() {
    // 创建底层Handler(这里用文本输出Handler)
    baseHandler := slog.NewTextHandler(os.Stdout, nil)
    steppedHandler := NewSteppedHandler(baseHandler)
    logger := slog.New(steppedHandler)

    logger.Info("Log message") // 输出包含step="STEP 1)"

    steppedHandler.PushLevel()
    logger.Info("Log message") // step="STEP 2.1)"
    
    // 衍生新的Logger实例
    newLogger := logger.With(slog.String("module", "auth"))
    newLogger.Info("Log message") // step="STEP 2.2)"

    steppedHandler.PushLevel()
    newLogger.Info("Log message") // step="STEP 2.3.1)"
    logger.Info("Log message")    // step="STEP 2.3.2)"

    steppedHandler.PopLevel()
    logger.Info("Log message")    // step="STEP 2.4)"

    steppedHandler.PopLevel()
    newLogger.Info("Log message") // step="STEP 3)"
}

方案优势

  • 优雅易用:用户仅需操作根SteppedHandler即可控制所有Logger的步骤层级,无需关心衍生实例的存在。
  • 符合slog规范:With/WithGroup返回独立的Handler实例,各自持有自己的属性/组配置,但共享步骤状态,完全适配slog的设计模式。
  • 单线程高效:因为仅支持单线程使用,无需加锁,避免了锁开销。

内容的提问来源于stack exchange,提问作者fx520

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 17:17:08