Go语言:structured logging with slog

Go 1.21引入的log/slog包终于给标准库带来了结构化日志。本文覆盖slog的核心概念、Handler接口、Logger分组,以及如何编写自定义Handler。

为什么需要结构化日志

传统log.Printf输出纯文本,在日志量大时几乎没法高效查询和分析:

2025/12/25 10:30:00 user login failed: invalid password, user=alice, ip=192.168.1.1

结构化日志把每条日志视为一组键值对,可以被ELK、Loki等系统直接索引:

{"time":"2025-12-25T10:30:00Z","level":"WARN","msg":"user login failed","user":"alice","ip":"192.168.1.1","reason":"invalid password"}

Go社区早就有zapzerolog等方案,但标准库一直缺席。slog填补了这个空白。

基本用法

package main

import "log/slog"

func main() {
    // 默认输出到stderr,Text格式
    slog.Info("server started", "port", 8080, "env", "production")
    // 输出: time=2025-12-25T10:00:00.000+08:00 level=INFO msg="server started" port=8080 env=production

    slog.Warn("high memory usage",
        "used_mb", 3800,
        "total_mb", 4096,
        "percent", 92.7,
    )
}

slog的API设计很简洁——函数名就是日志级别(InfoWarnErrorDebug),后面跟message和交替的key-value对。

如果觉得交替key-value容易出错,可以用强类型的slog.Attr

slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
    slog.String("method", "GET"),
    slog.String("path", "/api/users"),
    slog.Int("status", 200),
    slog.Duration("latency", 42*time.Millisecond),
)

Handler:JSON vs Text

slog通过Handler接口来控制输出格式和目标。内置两种:

// JSON Handler——适合生产环境,输出到stdout供日志收集器消费
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelDebug,
    // 添加调用源信息
    AddSource: true,
})
logger := slog.New(jsonHandler)
logger.Info("user created", "user_id", 42)
// {"time":"...","level":"INFO","source":{"function":"main.main","file":"main.go","line":15},"msg":"user created","user_id":42}

// Text Handler——适合开发环境,人类可读
textHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelDebug,
})
devLogger := slog.New(textHandler)

通常用环境变量切换:

func newLogger(env string) *slog.Logger {
    opts := &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }
    if env == "development" {
        opts.Level = slog.LevelDebug
        return slog.New(slog.NewTextHandler(os.Stderr, opts))
    }
    return slog.New(slog.NewJSONHandler(os.Stdout, opts))
}

Logger分组:With和WithGroup

With给Logger预绑定字段,避免每次都重复传:

// 请求级别的logger,预绑定request_id
reqLogger := logger.With("request_id", "abc-123", "user_id", 42)
reqLogger.Info("processing order", "order_id", 1001)
reqLogger.Error("payment failed", "order_id", 1001, "err", err)
// 两条日志都会带上request_id和user_id

WithGroup把后续字段归入一个嵌套组:

dbLogger := logger.WithGroup("db")
dbLogger.Info("query executed",
    "table", "users",
    "duration_ms", 12,
    "rows", 150,
)
// JSON输出: {"msg":"query executed","db":{"table":"users","duration_ms":12,"rows":150}}

在HTTP中间件中的典型用法:

func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            reqLogger := logger.With(
                "method", r.Method,
                "path", r.URL.Path,
                "remote", r.RemoteAddr,
            )
            // 将logger放入context
            ctx := context.WithValue(r.Context(), loggerKey, reqLogger)
            next.ServeHTTP(w, r.WithContext(ctx))
            reqLogger.Info("request completed",
                "duration", time.Since(start),
            )
        })
    }
}

自定义Handler

Handler接口很小:

type Handler interface {
    Enabled(context.Context, Level) bool
    Handle(context.Context, Record) error
    WithAttrs(attrs []Attr) Handler
    WithGroup(name string) Handler
}

写一个带颜色的开发用Handler:

type PrettyHandler struct {
    opts   slog.HandlerOptions
    mu     sync.Mutex
    w      io.Writer
    attrs  []slog.Attr
    groups []string
}

func NewPrettyHandler(w io.Writer, opts *slog.HandlerOptions) *PrettyHandler {
    if opts == nil {
        opts = &slog.HandlerOptions{}
    }
    return &PrettyHandler{w: w, opts: *opts}
}

func (h *PrettyHandler) Enabled(_ context.Context, level slog.Level) bool {
    minLevel := slog.LevelInfo
    if h.opts.Level != nil {
        minLevel = h.opts.Level.Level()
    }
    return level >= minLevel
}

var levelColors = map[slog.Level]string{
    slog.LevelDebug: "\033[36m", // cyan
    slog.LevelInfo:  "\033[32m", // green
    slog.LevelWarn:  "\033[33m", // yellow
    slog.LevelError: "\033[31m", // red
}

func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
    color := levelColors[r.Level]
    reset := "\033[0m"

    var buf bytes.Buffer
    fmt.Fprintf(&buf, "%s %s%-5s%s %s",
        r.Time.Format("15:04:05.000"),
        color, r.Level.String(), reset,
        r.Message,
    )

    // 输出预绑定的attrs
    for _, a := range h.attrs {
        fmt.Fprintf(&buf, " %s%s%s=%v", "\033[90m", a.Key, reset, a.Value)
    }
    // 输出本条记录的attrs
    r.Attrs(func(a slog.Attr) bool {
        fmt.Fprintf(&buf, " %s%s%s=%v", "\033[90m", a.Key, reset, a.Value)
        return true
    })
    buf.WriteByte('\n')

    h.mu.Lock()
    defer h.mu.Unlock()
    _, err := h.w.Write(buf.Bytes())
    return err
}

func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    newH := *h
    newH.attrs = append(slices.Clone(h.attrs), attrs...)
    return &newH
}

func (h *PrettyHandler) WithGroup(name string) slog.Handler {
    newH := *h
    newH.groups = append(slices.Clone(h.groups), name)
    return &newH
}

性能注意事项

slog的设计在性能上做了考量:

  • Enabled方法允许快速跳过低级别日志,避免参数构造开销
  • slog.LogAttrs比交替key-value更快,因为省去了反射
  • Record使用内联数组(前5个Attr不分配堆内存)

如果对性能有极致要求,仍然可以选择zap。但对大多数服务来说,slog够用了,而且零依赖。

小结

slog给Go的日志实践提供了一个坚实的标准基础。Handler接口足够灵活,可以适配各种输出需求;With/WithGroup的设计让上下文传播变得自然。新项目直接用slog,老项目可以逐步迁移——毕竟,标准库的东西不会消失。