Go 1.21 slog:标准库的结构化日志
日志,是每个后端工程师绕不开的话题。你可能用过 logrus、zap、zerolog 这些第三方库,也可能还在用标准库的 log 包拼接字符串。不管怎样,Go 1.21 给你带来了一个"迟到但正确"的礼物——log/slog 包。
slog 是 structured log 的缩写,即结构化日志。它不是简单地把文本丢到终端,而是以键值对的方式组织日志信息,让日志变得可搜索、可过滤、可机器解析。想想看,当你面对每天几十 GB 的日志数据,拿着 grep 去找一行 user_id=42 的记录时,结构化日志就是你的救命稻草。
这个包的提案最早可以追溯到 2022 年初,由 Go 团队的核心成员 Jonathan Amsterdam 主导设计。在设计过程中,团队广泛参考了社区中 zap、zerolog、logrus 等优秀日志库的经验教训,最终提炼出了一套简洁而强大的 API。slog 的核心设计原则是:性能足够好、API 足够简洁、可扩展性强、与标准库深度整合。这些原则贯穿了整个包的设计,从 Value 的内部表示到 Handler 接口的抽象,每一处都体现了 Go 团队对"少即是多"哲学的坚持。
本文将从零开始,带你全面掌握 slog 的方方面面。无论你是刚接触结构化日志的新手,还是已经熟练使用 zap 或 zerolog 的老手,都能在这篇文章中找到有价值的内容。
为什么需要结构化日志?
在开始写代码之前,先让我们聊聊"为什么"。
传统日志的痛点
让我们先看一段再熟悉不过的代码:
package main
import (
"fmt"
"time"
)
func main() {
userID := 42
action := "login"
ip := "192.168.1.100"
// 传统文本日志——全靠字符串拼接
fmt.Printf("[%s] INFO: user %d performed action %s from ip %s\n",
time.Now().Format(time.RFC3339), userID, action, ip)
// 输出:[2023-03-10T10:45:00+08:00] INFO: user 42 performed action login from ip 192.168.1.100
}
这段日志看起来还行,对吧?在开发阶段,你一眼就能看懂。但当你把它放到生产环境中,问题就来了:
- 解析困难:如果你想找出所有
user_id=42的日志,得用正则表达式去匹配user 42这个模式。但如果日志里还有user 420、user 4200,你的正则就会误匹配。更糟糕的是,如果某天有人改了日志格式,把user 42改成了userId=42,你的所有查询脚本都会失效。 - 格式不统一:不同开发者写的日志格式五花八门,有人用
user=42,有人用user_id: 42,还有人用userId 42。在一个大型项目中,这种不一致性会让日志分析变成一场噩梦。 - 无法聚合:当日志被收集到 ELK(Elasticsearch + Logstash + Kibana)、Loki、Splunk 这样的系统中,非结构化的文本几乎无法高效查询。你想要的是
SELECT * FROM logs WHERE user_id = 42 AND action = 'login',但实际得到的是grep "user 42" logs/*.log | grep "login"。 - 缺乏上下文:传统日志往往只记录了"发生了什么",但没有记录"在什么上下文中发生"。比如一个请求经过了 10 个微服务,你怎么把这些日志串联起来?靠
request_id?那你的每条日志都得手动加上这个字段。
结构化日志的解决方案
现在让我们看看 slog 是如何解决这些问题的:
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user action",
"user_id", 42,
"action", "login",
"ip", "192.168.1.100",
)
// 输出:{"time":"2023-03-10T10:45:00+08:00","level":"INFO","msg":"user action","user_id":42,"action":"login","ip":"192.168.1.100"}
}
看到了吗?输出是标准的 JSON 格式,每个字段都有明确的键名。这样的日志可以直接被日志系统索引、查询和分析。你可以在 Kibana 中写 user_id: 42 AND action: "login",在 Loki 中写 {user_id="42", action="login"},都能精确匹配。
更重要的是,结构化日志让你的日志变得可预测。无论谁写的代码,输出的格式都是一致的。这种一致性对于团队协作和系统运维来说,价值巨大。想象一下,当你半夜被报警叫醒,打开日志系统,看到整齐划一的 JSON 日志,每个字段都清晰可辨——那种感觉,就像在黑暗中找到了手电筒。
slog 核心概念
slog 的设计非常简洁,核心只有三个概念:
- Logger:日志记录器,你调用它的方法来写日志。Logger 是面向开发者的 API,它提供了
Debug、Info、Warn、Error四个级别的日志方法,以及With、WithGroup等用于预设上下文的方法。 - Handler:处理器,决定日志怎么输出(JSON?纯文本?写到文件?发到远程?)。Handler 是
slog可扩展性的核心,你可以通过实现Handler接口来创建自定义的输出目标。 - Attr:属性,键值对形式的日志字段。Attr 由一个字符串键和一个 Value 组成,Value 内部使用了高效的类型表示,避免了接口分配。
三者的关系很直观:
你的代码 → Logger(决定"记什么") → Handler(决定"怎么输出") → 输出目标
这种分离设计让 slog 既简单又灵活。你可以使用默认的 Handler 快速上手,也可以根据业务需求定制自己的 Handler。比如,你可以写一个 Handler 把日志发送到 Kafka,另一个 Handler 把日志写入数据库,还有一个 Handler 把日志推送到监控系统。
基础用法
使用默认 Logger
slog 提供了包级别的便捷函数,开箱即用。这对于快速原型开发和简单的命令行工具来说非常方便,你不需要显式地创建 Logger 对象,直接调用包级别的函数即可。
package main
import "log/slog"
func main() {
// 默认使用 TextHandler,输出到 os.Stderr
slog.Info("server started", "port", 8080)
slog.Warn("disk usage high", "usage", "85%")
slog.Error("connection failed", "host", "db.example.com", "error", "timeout")
slog.Debug("this won't show by default") // 默认级别是 Info,Debug 不会输出
}
输出结果:
time=2023-03-10T10:45:00.000+08:00 level=INFO msg="server started" port=8080
time=2023-03-10T10:45:00.000+08:00 level=WARN msg="disk usage high" usage=85%
time=2023-03-10T10:45:00.000+08:00 level=ERROR msg="connection failed" host=db.example.com error=timeout
默认输出格式是 key=value 的文本形式,适合开发调试。
四种日志级别
slog 内置了四个日志级别:
package main
import "log/slog"
func main() {
slog.Debug("调试信息——开发时才需要看")
slog.Info("普通信息——正常运行状态")
slog.Warn("警告信息——需要注意但不影响运行")
slog.Error("错误信息——出了问题需要处理")
}
级别从低到高分别是 Debug < Info < Warn < Error。默认级别是 Info,也就是说 Debug 级别的日志会被丢弃。
自定义 Logger
默认的 Logger 往往不够用,我们需要自己创建:
package main
import (
"log/slog"
"os"
)
func main() {
// 创建一个 JSON 格式的 Logger
jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // 开启 Debug 级别
}))
jsonLogger.Debug("cache miss", "key", "user:42", "cache_name", "redis-01")
jsonLogger.Info("request processed",
"method", "GET",
"path", "/api/users/42",
"status", 200,
"duration_ms", 15,
)
}
输出:
{"time":"2023-03-10T10:45:00+08:00","level":"DEBUG","msg":"cache miss","key":"user:42","cache_name":"redis-01"}
{"time":"2023-03-10T10:45:00+08:00","level":"INFO","msg":"request processed","method":"GET","path":"/api/users/42","status":200,"duration_ms":15}
Handler 详解
Handler 是 slog 的灵魂。它实现了 slog.Handler 接口:
type Handler interface {
Enabled(context.Context, Level) bool
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
TextHandler vs JSONHandler
slog 内置了两个 Handler:
package main
import (
"log/slog"
"os"
)
func main() {
// TextHandler——人类友好的键值对格式
textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
textLogger.Info("hello", "name", "world", "count", 42)
// 输出:time=2023-03-10T10:45:00+08:00 level=INFO msg=hello name=world count=42
// JSONHandler——机器友好的 JSON 格式
jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
jsonLogger.Info("hello", "name", "world", "count", 42)
// 输出:{"time":"...","level":"INFO","msg":"hello","name":"world","count":42}
}
选择建议:
- 开发环境用
TextHandler,终端阅读方便。 - 生产环境用
JSONHandler,方便日志采集系统解析。
HandlerOptions 配置
package main
import (
"log/slog"
"os"
)
func main() {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // 最低日志级别
AddSource: true, // 添加源代码位置
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 自定义属性替换——比如修改时间格式
if a.Key == slog.TimeKey {
t := a.Value.Time()
a.Value = slog.StringValue(t.Format("2006-01-02 15:04:05"))
}
// 修改级别名称
if a.Key == slog.LevelKey {
level := a.Value.Any().(slog.Level)
switch level {
case slog.LevelDebug:
a.Value = slog.StringValue("🔍 DEBUG")
case slog.LevelInfo:
a.Value = slog.StringValue("📋 INFO")
case slog.LevelWarn:
a.Value = slog.StringValue("⚠️ WARN")
case slog.LevelError:
a.Value = slog.StringValue("❌ ERROR")
}
}
return a
},
})
logger := slog.New(handler)
logger.Info("server ready", "port", 8080)
logger.Debug("loading config", "file", "config.yaml")
}
ReplaceAttr 是一个非常强大的功能,让你可以在日志输出前对任何属性做转换。比如脱敏、格式化、甚至直接删掉某个字段。
实战:多 Handler 输出
一个常见需求是:普通日志输出到 stdout,错误日志输出到 stderr。
package main
import (
"context"
"io"
"log/slog"
"os"
)
// MultiHandler 将日志分发到不同的 Handler
type MultiHandler struct {
handlers []slog.Handler
}
func NewMultiHandler(handlers ...slog.Handler) *MultiHandler {
return &MultiHandler{handlers: handlers}
}
func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h.handlers {
if handler.Enabled(ctx, level) {
return true
}
}
return false
}
func (h *MultiHandler) Handle(ctx context.Context, record slog.Record) error {
for _, handler := range h.handlers {
if handler.Enabled(ctx, record.Level) {
if err := handler.Handle(ctx, record); err != nil {
return err
}
}
}
return nil
}
func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithAttrs(attrs)
}
return NewMultiHandler(newHandlers...)
}
func (h *MultiHandler) WithGroup(name string) slog.Handler {
newHandlers := make([]slog.Handler, len(h.handlers))
for i, handler := range h.handlers {
newHandlers[i] = handler.WithGroup(name)
}
return NewMultiHandler(newHandlers...)
}
func main() {
// Info 及以上 → stdout
stdoutHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
// Error 及以上 → stderr
stderrHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelError,
})
// 同时写入日志文件
logFile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
defer logFile.Close()
fileHandler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
multi := NewMultiHandler(stdoutHandler, stderrHandler, fileHandler)
logger := slog.New(multi)
logger.Info("this goes to stdout and file")
logger.Error("this goes to stdout, stderr, and file")
logger.Debug("this only goes to file")
}
这个例子展示了 slog 的扩展性——通过实现 Handler 接口,你可以把日志发送到任何地方:Kafka、Loki、Datadog,甚至数据库。
Attr:结构化日志的基石
slog.Attr 是键值对的核心抽象:
type Attr struct {
Key string
Value Value
}
创建 Attr 的多种方式
package main
import (
"log/slog"
"os"
"time"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 方式一:直接传键值对(最常用)
logger.Info("request",
"method", "GET",
"path", "/api/users",
"status", 200,
)
// 方式二:使用类型化的构造函数
logger.Info("request",
slog.String("method", "GET"),
slog.String("path", "/api/users"),
slog.Int("status", 200),
slog.Duration("latency", 15*time.Millisecond),
slog.Bool("cached", false),
)
// 方式三:使用 Any(任意类型)
logger.Info("request",
slog.Any("headers", map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer xxx",
}),
)
// 方式四:使用 Attr 结构体
logger.Info("request",
slog.Attr{Key: "method", Value: slog.StringValue("GET")},
slog.Attr{Key: "status", Value: slog.IntValue(200)},
)
}
Value 的类型系统
slog.Value 不是简单的 interface{},它有自己内部的类型系统,这是 slog 性能的关键:
package main
import (
"fmt"
"log/slog"
"time"
)
func main() {
// slog.Value 有专门的 Kind 来标识类型
kinds := []slog.Value{
slog.StringValue("hello"),
slog.IntValue(42),
slog.Float64Value(3.14),
slog.BoolValue(true),
slog.TimeValue(time.Now()),
slog.DurationValue(time.Second),
slog.GroupValue(
slog.String("name", "Alice"),
slog.Int("age", 30),
),
}
for _, v := range kinds {
fmt.Printf("Kind: %-10v String: %s\n", v.Kind(), v.String())
}
}
这种设计的好处是:slog 不需要在运行时做反射(reflection),类型信息直接编码在 Kind 里,所以性能非常好。
Group:给日志分组
当你的日志字段很多时,Group 可以帮你组织层次结构:
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 使用 Group 嵌套结构
logger.Info("http request",
slog.Group("request",
slog.String("method", "GET"),
slog.String("path", "/api/users/42"),
slog.String("ip", "192.168.1.100"),
),
slog.Group("response",
slog.Int("status", 200),
slog.Int("bytes", 1024),
slog.String("duration", "15ms"),
),
)
// 输出:
// {"time":"...","level":"INFO","msg":"http request",
// "request":{"method":"GET","path":"/api/users/42","ip":"192.168.1.100"},
// "response":{"status":200,"bytes":1024,"duration":"15ms"}}
}
命名 Group
你也可以给 Logger 预设一个 Group,后续所有日志字段都会嵌套在里面:
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 创建一个带 request group 的 logger
reqLogger := logger.WithGroup("request")
reqLogger.Info("incoming", "method", "GET", "path", "/api")
// 输出:{"time":"...","level":"INFO","msg":"incoming","request":{"method":"GET","path":"/api"}}
// 再嵌套一层
httpLogger := reqLogger.WithGroup("http")
httpLogger.Info("detail", "proto", "HTTP/2.0", "host", "example.com")
// 输出:{"time":"...","level":"INFO","msg":"detail","request":{"http":{"proto":"HTTP/2.0","host":"example.com"}}}
}
With:预设公共字段
在很多场景下,你希望某些字段出现在所有日志中,比如 request_id、service_name 等。With 方法正是为此设计的:
package main
import (
"log/slog"
"os"
)
func handleRequest(logger *slog.Logger, requestID string) {
// 给当前请求的 logger 添加 request_id
reqLogger := logger.With("request_id", requestID)
reqLogger.Info("handling request")
reqLogger.Debug("parsing body")
reqLogger.Info("request completed", "status", 200)
// 所有日志都会带上 request_id:
// {"time":"...","level":"INFO","msg":"handling request","service":"user-api","request_id":"abc-123"}
// {"time":"...","level":"DEBUG","msg":"parsing body","service":"user-api","request_id":"abc-123"}
// {"time":"...","level":"INFO","msg":"request completed","service":"user-api","request_id":"abc-123","status":200}
}
func main() {
// 全局 logger,带上 service 信息
baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
With("service", "user-api", "version", "1.2.3")
handleRequest(baseLogger, "abc-123")
}
With 返回的是一个新的 Logger,不会影响原来的 Logger,这点非常重要——它是不可变的(immutable)。
实战场景
场景一:HTTP 中间件
package main
import (
"log/slog"
"net/http"
"os"
"time"
)
// responseWriter 包装器,用于捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
bytes int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
return n, err
}
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()
// 为每个请求生成唯一 logger
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = "unknown"
}
reqLogger := logger.With(
"request_id", reqID,
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
)
reqLogger.Info("request started")
// 包装 ResponseWriter
wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
// 调用下一个 handler
next.ServeHTTP(wrapped, r)
duration := time.Since(start)
reqLogger.Info("request completed",
"status", wrapped.statusCode,
"bytes", wrapped.bytes,
"duration", duration.String(),
"duration_ms", duration.Milliseconds(),
)
})
}
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
handler := LoggingMiddleware(logger)(mux)
logger.Info("server starting", "port", 8080)
http.ListenAndServe(":8080", handler)
}
场景二:实现 LogValuer 接口
对于复杂的结构体,你可以实现 slog.LogValuer 接口,让 slog 知道如何序列化它:
package main
import (
"log/slog"
"os"
)
// User 用户模型
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"` // 敏感信息!
Role string `json:"role"`
}
// LogValue 实现 slog.LogValuer 接口
// 注意:这里绝对不能把密码写进日志!
func (u User) LogValue() slog.Value {
return slog.GroupValue(
slog.Int64("user_id", u.ID),
slog.String("name", u.Name),
slog.String("role", u.Role),
// 故意不记录 email 和 password
)
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
user := User{
ID: 42,
Name: "Alice",
Email: "alice@example.com",
Password: "super-secret-password", // 绝对不会出现在日志中
Role: "admin",
}
logger.Info("user logged in", "user", user)
// 输出:{"time":"...","level":"INFO","msg":"user logged in","user":{"user_id":42,"name":"Alice","role":"admin"}}
// 看!密码和邮箱都没有出现在日志中
}
这是一个非常实用的模式。在任何生产系统中,你都需要对敏感信息做脱敏处理,LogValuer 接口让这个过程变得优雅而安全。
场景三:替换默认 Logger
slog 可以替换标准库的默认 logger:
package main
import (
"log"
"log/slog"
"os"
)
func main() {
// 创建自定义 slog handler
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
// 用 slog 替换标准库的默认 logger
logger := slog.NewLogLogger(handler, slog.LevelInfo)
log.SetDefault(logger)
// 现在标准库的 log 也会输出结构化日志
log.Println("this is now structured!")
// 输出:{"time":"...","level":"INFO","msg":"this is now structured!"}
}
这意味着你的项目中即使有遗留代码使用 log.Println,也能享受到结构化日志的好处。
性能分析
性能是日志库的核心指标之一。来看看 slog 的表现。
基准测试
package main
import (
"io"
"log/slog"
"testing"
)
func BenchmarkSlogJSON(b *testing.B) {
logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
logger.Info("request completed",
"method", "GET",
"path", "/api/users",
"status", 200,
"duration_ms", 15,
)
}
}
func BenchmarkSlogText(b *testing.B) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
logger.Info("request completed",
"method", "GET",
"path", "/api/users",
"status", 200,
"duration_ms", 15,
)
}
}
func BenchmarkSlogWithAttrs(b *testing.B) {
base := slog.New(slog.NewJSONHandler(io.Discard, nil)).
With("service", "user-api", "version", "1.0")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
base.Info("request completed",
"method", "GET",
"path", "/api/users",
"status", 200,
)
}
}
在我的机器上(Apple M1 Pro),结果大致如下:
BenchmarkSlogJSON-10 5000000 240 ns/op 1 allocs/op
BenchmarkSlogText-10 4000000 280 ns/op 2 allocs/op
BenchmarkSlogWithAttrs-10 4500000 260 ns/op 1 allocs/op
与其他日志库对比
| 库 | ns/op | allocs/op | 特点 |
|---|---|---|---|
log/slog (JSON) | ~240 | 1 | 标准库,零依赖 |
go.uber.org/zap | ~150 | 0 | 极致性能,API 略复杂 |
rs/zerolog | ~170 | 0 | 性能好,链式 API |
sirupsen/logrus | ~2800 | 24 | 功能丰富,但性能差 |
slog 的性能介于 zap/zerolog 和 logrus 之间。它不如 zap 和 zerolog 那么极致(因为后两者用了大量 unsafe 和对象池优化),但比 logrus 快了一个数量级。对于绝大多数应用场景,slog 的性能绰绰有余。
关键点:slog 的 Value 类型避免了接口分配(interface allocation),这是它能做到 1 alloc/op 的核心原因。而 logrus 大量使用 map[string]interface{},每次日志调用都会产生大量的堆分配。
与第三方日志库的对比
功能对比矩阵
| 功能 | slog | zap | zerolog | logrus |
|---|---|---|---|---|
| 结构化日志 | ✅ | ✅ | ✅ | ✅ |
| JSON 输出 | ✅ | ✅ | ✅ | ✅ |
| 自定义级别 | ✅ | ✅ | ✅ | ✅ |
| Context 支持 | ✅ | ❌ | ❌ | ✅ |
| 标准库兼容 | ✅ | ❌ | ❌ | 部分 |
| 零依赖 | ✅ | ❌ | ❌ | ❌ |
| 高性能 | 良好 | 极致 | 极致 | 一般 |
| 学习曲线 | 低 | 中 | 中 | 低 |
何时用 slog?何时用第三方?
选 slog 的理由:
- 标准库自带,不需要额外依赖
- API 设计简洁,团队学习成本低
- 对于中小规模项目,性能完全够用
- 官方维护,长期稳定有保障
选 zap/zerolog 的理由:
- 高流量服务,日志是性能瓶颈
- 需要零分配(zero-allocation)的极致性能
- 已经深度使用了这些库的生态
一个务实的建议:如果你的项目刚刚开始,没有历史包袱,优先使用 slog。它可能不会是最快的,但一定是最"正统"的。随着 Go 生态的发展,slog 的 Handler 生态会越来越丰富。
最佳实践
1. 统一的字段命名
package main
import (
"log/slog"
"os"
)
// 定义常量,确保字段名一致
const (
FieldRequestID = "request_id"
FieldUserID = "user_id"
FieldMethod = "method"
FieldPath = "path"
FieldStatus = "status"
FieldDuration = "duration_ms"
FieldError = "error"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 使用常量而不是字符串字面量
logger.Info("request",
FieldMethod, "GET",
FieldPath, "/api/users",
FieldStatus, 200,
FieldDuration, 15,
)
}
2. 利用 Context 传递 Logger
package main
import (
"context"
"log/slog"
"os"
)
// contextKey 是自定义的 context key 类型
type contextKey string
const loggerKey contextKey = "logger"
// LoggerFromContext 从 context 中获取 logger
func LoggerFromContext(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
// WithLogger 将 logger 放入 context
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
func processOrder(ctx context.Context, orderID string) {
logger := LoggerFromContext(ctx)
logger.Info("processing order", "order_id", orderID)
// 无需显式传递 logger,通过 context 自然传递
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
With("service", "order-service")
ctx := WithLogger(context.Background(), logger)
// 在调用链的深处也能拿到带公共字段的 logger
processOrder(ctx, "ORD-2023-001")
}
3. 延迟求值(Lazy Evaluation)
当计算日志字段代价较高时,使用 slog.LogValuer 做延迟求值:
package main
import (
"log/slog"
"os"
)
// LazyValue 延迟计算的值
type LazyValue struct {
fn func() any
}
func (l LazyValue) LogValue() slog.Value {
return slog.AnyValue(l.fn())
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// 假设 GetSystemStats() 很昂贵
// 只有日志真正被输出时才会调用
logger.Info("system status",
"stats", LazyValue{fn: func() any {
return map[string]int{
"goroutines": 150,
"heap_mb": 256,
}
}},
)
}
4. 设置全局默认 Logger
package main
import (
"log/slog"
"os"
)
func init() {
// 在 init 或 main 中设置全局默认 logger
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 移除源码位置,生产环境不需要
if a.Key == slog.SourceKey {
return slog.Attr{}
}
return a
},
})
slog.SetDefault(slog.New(handler))
}
func main() {
// 任何地方都可以直接使用 slog.Info
slog.Info("application started")
}
小结
slog 的引入,标志着 Go 标准库在日志领域终于"追上了时代"。它不追求最极致的性能,也不追求最丰富的功能,而是追求正确的抽象。
回顾一下核心要点:
- Logger 负责"记什么",Handler 负责"怎么输出",Attr 负责"记什么内容"。
- 开发环境用
TextHandler,生产环境用JSONHandler。 - 用
With预设公共字段,用Group组织嵌套结构。 - 实现
LogValuer接口做敏感信息脱敏。 - 性能优于
logrus,接近zap/zerolog,对大多数项目完全够用。
如果你正在开始一个新的 Go 项目,不妨直接拥抱 slog。它就像 Go 语言本身一样——简单、够用、可靠。
延伸阅读与资源
- Go 1.21 Release Notes - slog:官方发布说明,了解
slog的设计背景。 - log/slog 官方文档:完整的 API 文档,包括所有类型和方法的详细说明。
- Structured Logging Proposal:原始的 GitHub Issue,记录了设计讨论和决策过程。
- slog Handler 生态:社区贡献的各种 Handler 实现,包括 Loki、Datadog、CloudWatch 等。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。