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社区早就有zap、zerolog等方案,但标准库一直缺席。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设计很简洁——函数名就是日志级别(Info、Warn、Error、Debug),后面跟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,老项目可以逐步迁移——毕竟,标准库的东西不会消失。