如何优雅实现支持嵌套步骤递增的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
相关产品推荐
相关产品推荐

