如何全面掌握Go语言中使用slog库进行日志记录的入门知识?

摘要:简介 slog 是 Go 1.21 引入的官方结构化日志库(Structured Logging)。它结束了 Go 标准库只有简单 log 包的历史,让我们可以直接输出 JSON 或 Key-Value 格式的日志,非常适合对接 ELK、G
简介 slog 是 Go 1.21 引入的官方结构化日志库(Structured Logging)。它结束了 Go 标准库只有简单 log 包的历史,让我们可以直接输出 JSON 或 Key-Value 格式的日志,非常适合对接 ELK、Grafana Loki 等日志分析系统。 相较于第三方日志库如 zap、logrus,slog 的优势在于: 零依赖:作为标准库的一部分,无需引入第三方依赖 官方维护:长期稳定,API 变更有 Go 兼容性承诺保障 接口简洁:API 设计清晰,学习成本低 可扩展:通过自定义 Handler 可以实现各种定制需求 基本使用 slog 用起来非常简单。默认输出到标准错误流(os.Stderr),格式为普通文本。 package main import ( "fmt" "log/slog" ) func main() { slog.Debug("Hello world") slog.Info("Hello world") slog.Warn("Hello world") slog.Error("Hello world") slog.Info("this is a message", "name", "zhangsan") age := 8 slog.Warn(fmt.Sprintf("这是 %d 岁?", age)) } 运行输出: $ go run main.go 2026/02/15 11:52:24 INFO Hello world 2026/02/15 11:52:24 WARN Hello world 2026/02/15 11:52:24 ERROR Hello world 2026/02/15 11:52:24 INFO this is a message name=zhangsan 2026/02/15 11:52:24 WARN 这是 8 岁? 注意:默认的 slog logger 日志级别为 INFO,因此 Debug 级别的日志不会输出。 日志级别 slog 定义了四个日志级别,从低到高依次为: 级别 常量 说明 DEBUG slog.LevelDebug 调试信息,开发环境使用 INFO slog.LevelInfo 常规信息 WARN slog.LevelWarn 警告信息 ERROR slog.LevelError 错误信息 输出 JSON 格式 slog 可以输出 JSON 格式,便于与 ELK、Grafana Loki 等日志系统集成。 以下示例演示了如何修改默认的时间戳格式和调用源输出格式,并将其设置为默认 logger: package main import ( "fmt" "log/slog" "os" "time" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ AddSource: true, // 添加调用源信息 Level: slog.LevelDebug, // 设置日志级别 ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { // 自定义时间格式 if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.Format(time.RFC3339)) } } // 简化调用源信息,只保留文件名和行号 if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) shortFile := source.File for i := len(source.File) - 1; i > 0; i-- { if source.File[i] == '/' { shortFile = source.File[i+1:] break } } return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line)) } return a }, })) jsonLogger.Debug("Hello world") jsonLogger.Info("Hello world") jsonLogger.Warn("Hello world") jsonLogger.Error("Hello world") jsonLogger.Info("this is a message", "name", "zhangsan") age := 8 jsonLogger.Warn(fmt.Sprintf("这是 %d 岁?", age)) // 替换默认 logger slog.SetDefault(jsonLogger) slog.Debug("Hello world") slog.Info("Hello world") slog.Warn("Hello world") slog.Error("Hello world") slog.Info("this is a message", "name", "zhangsan") age = 9 slog.Warn(fmt.Sprintf("这是 %d 岁?", age)) } 运行输出: $ go run main.go {"time":"2026-02-15T12:07:32+08:00","level":"DEBUG","source":"main.go:38","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:39","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:40","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"ERROR","source":"main.go:41","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:43","msg":"this is a message","name":"zhangsan"} {"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:46","msg":"这是 8 岁?"} {"time":"2026-02-15T12:07:32+08:00","level":"DEBUG","source":"main.go:50","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:51","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:52","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"ERROR","source":"main.go:53","msg":"Hello world"} {"time":"2026-02-15T12:07:32+08:00","level":"INFO","source":"main.go:55","msg":"this is a message","name":"zhangsan"} {"time":"2026-02-15T12:07:32+08:00","level":"WARN","source":"main.go:58","msg":"这是 9 岁?"} HandlerOptions 详解 HandlerOptions 提供了三个配置项: 字段 类型 说明 AddSource bool 是否添加调用源信息(文件名和行号) Level slog.Leveler 最低日志级别,低于此级别的日志将被忽略 ReplaceAttr func([]string, slog.Attr) slog.Attr 用于修改或替换属性的回调函数 With 注入通用属性 创建 Logger 时,可以用 With 方法为 logger 添加通用属性。这些属性会自动附加到每条日志记录中,适合注入服务名、环境、版本等上下文信息。 package main import ( "fmt" "log/slog" "os" "time" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.Format(time.RFC3339)) } } if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) shortFile := source.File for i := len(source.File) - 1; i > 0; i-- { if source.File[i] == '/' { shortFile = source.File[i+1:] break } } return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line)) } return a }, })).With("logger", "json", "env", "production") jsonLogger.Debug("Hello world") jsonLogger.Info("Hello world") jsonLogger.Warn("Hello world") jsonLogger.Error("Hello world") jsonLogger.Info("this is a message", "name", "zhangsan") } 运行输出: $ go run main.go {"time":"2026-02-15T13:24:38+08:00","level":"DEBUG","source":"main.go:42","msg":"Hello world","logger":"json","env":"production"} {"time":"2026-02-15T13:24:38+08:00","level":"INFO","source":"main.go:43","msg":"Hello world","logger":"json","env":"production"} {"time":"2026-02-15T13:24:38+08:00","level":"WARN","source":"main.go:44","msg":"Hello world","logger":"json","env":"production"} {"time":"2026-02-15T13:24:38+08:00","level":"ERROR","source":"main.go:45","msg":"Hello world","logger":"json","env":"production"} {"time":"2026-02-15T13:24:38+08:00","level":"INFO","source":"main.go:47","msg":"this is a message","logger":"json","env":"production","name":"zhangsan"} 使用 Group 对属性分组 当日志属性较多时,可以使用 slog.Group 将相关属性组织在一起,使输出结构更清晰: package main import ( "fmt" "log/slog" "os" "time" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.Format(time.RFC3339)) } } if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) shortFile := source.File for i := len(source.File) - 1; i > 0; i-- { if source.File[i] == '/' { shortFile = source.File[i+1:] break } } return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line)) } return a }, })) jsonLogger = jsonLogger.With("logger", "json") // 使用 Group 组织相关属性 jsonLogger.Info("系统状态", slog.Group("metrics", slog.Int("cpu", 4), slog.Float64("memPercent", 2.33), ), slog.Group("request", slog.String("method", "GET"), slog.String("path", "/api/users"), ), ) } 运行输出: $ go run main.go {"time":"2026-02-15T13:30:08+08:00","level":"INFO","source":"main.go:43","msg":"系统状态","logger":"json","metrics":{"cpu":4,"memPercent":2.33},"request":{"method":"GET","path":"/api/users"}} 高性能场景使用 LogAttrs 如果需要在高性能循环中打印日志,建议使用 LogAttrs 方法。它使用强类型属性(slog.Attr),避免了反射带来的性能开销。 package main import ( "context" "log/slog" "os" "time" ) func main() { jsonLogger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.Format(time.RFC3339)) } } if a.Key == slog.SourceKey { source := a.Value.Any().(*slog.Source) shortFile := source.File for i := len(source.File) - 1; i > 0; i-- { if source.File[i] == '/' { shortFile = source.File[i+1:] break } } return slog.String("source", fmt.Sprintf("%s:%d", shortFile, source.Line)) } return a }, })).With("logger", "json") for i := range 5 { jsonLogger.LogAttrs( context.Background(), slog.LevelInfo, "执行遍历", slog.Int("round", i), slog.String("task_name", "cleanup"), slog.Duration("duration", time.Second*time.Duration(i+1)), ) } } 运行输出: $ go run main.go {"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"执行遍历","logger":"json","round":0,"task_name":"cleanup","duration":1000000000} {"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"执行遍历","logger":"json","round":1,"task_name":"cleanup","duration":2000000000} {"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"执行遍历","logger":"json","round":2,"task_name":"cleanup","duration":3000000000} {"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"执行遍历","logger":"json","round":3,"task_name":"cleanup","duration":4000000000} {"time":"2026-02-15T13:38:21+08:00","level":"INFO","source":"main.go:45","msg":"执行遍历","logger":"json","round":4,"task_name":"cleanup","duration":5000000000} 性能对比 根据官方基准测试,LogAttrs 相比普通方法调用有约 30% 的性能提升: 方法 内存分配 性能 slog.Info(msg, "key", value) 有额外分配 基准 slog.LogAttrs(ctx, level, msg, attrs...) 零额外分配 快约 30% 提取 Context 中的链路信息 slog 提供了 InfoContext、WarnContext 等方法,可以从 context.Context 中提取数据。默认情况下,这些方法不会自动提取 context 中的值,需要通过自定义 Handler 来实现。 自定义 ContextHandler 以下示例实现了一个自定义 Handler,用于从 context 中提取 TraceID: package main import ( "context" "log/slog" "os" ) type contextKey string const TraceIDKey contextKey = "trace_id" // ContextHandler 包装一个 slog.Handler,在处理日志时自动从 context 中提取 TraceID type ContextHandler struct { slog.Handler } func (h *ContextHandler) Handle(ctx context.Context, record slog.Record) error { if ctx != nil { if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" { record.AddAttrs(slog.String(string(TraceIDKey), traceID)) } } return h.Handler.Handle(ctx, record) } func main() { baseHandler := slog.NewJSONHandler(os.Stdout, nil) handler := &ContextHandler{Handler: baseHandler} jsonLogger := slog.New(handler) slog.SetDefault(jsonLogger) ctx := context.WithValue(context.Background(), TraceIDKey, "abc123-def456") slog.InfoContext(ctx, "hello world") slog.WarnContext(ctx, "something happened", "user", "zhangsan") } 运行输出: $ go run main.go | python3 -m json.tool { "time": "2026-02-15T13:56:43.086323769+08:00", "level": "INFO", "msg": "hello world", "trace_id": "abc123-def456" } { "time": "2026-02-15T13:56:43.086323769+08:00", "level": "WARN", "msg": "something happened", "user": "zhangsan", "trace_id": "abc123-def456" } 在 Gin 框架中使用 slog 在 Gin 中使用 slog 的 context 能力,通常的做法是编写一个中间件来注入 TraceID,并配合自定义 slog.Handler 来提取它。 package main import ( "context" "log/slog" "net" "net/http" "net/http/httputil" "os" "runtime/debug" "strings" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type contextKey string const TraceIDKey contextKey = "trace_id" // ContextHandler 从 context 中提取 TraceID 并添加到日志中 type ContextHandler struct { slog.Handler } func (h *ContextHandler) Handle(ctx context.Context, record slog.Record) error { if ctx != nil { if traceID, ok := ctx.Value(TraceIDKey).(string); ok && traceID != "" { record.AddAttrs(slog.String(string(TraceIDKey), traceID)) } } return h.Handler.Handle(ctx, record) } // SlogMiddleware 是一个 Gin 中间件,用于注入 TraceID func SlogMiddleware() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() // 优先从请求头获取 TraceID,没有则生成新的 traceID := c.GetHeader("X-Trace-ID") if traceID == "" { traceID = uuid.New().String() } // 将 TraceID 注入到标准的 context.Context 中 // 注意:Gin 的 c.Set 只在 Gin 内部生效,slog 需要标准库的 Context ctx := context.WithValue(c.Request.Context(), TraceIDKey, traceID) c.Request = c.Request.WithContext(ctx) // 将 TraceID 写入响应头,方便客户端追踪 c.Header("X-Trace-ID", traceID) c.Next() // 请求结束后的汇总日志 slog.InfoContext(c.Request.Context(), "Request completed", slog.String("method", c.Request.Method), slog.String("path", c.Request.URL.Path), slog.Int("status", c.Writer.Status()), slog.Int("body_size", c.Writer.Size()), slog.Duration("latency", time.Since(start)), ) } } // SlogRecovery 是一个自定义的恢复中间件 // 它会捕获 Panic,记录堆栈信息,并使用 slog.ErrorContext 输出 func SlogRecovery() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { // 检查是否是连接中断(broken pipe) var brokenPipe bool if ne, ok := err.(*net.OpError); ok { if se, ok := ne.Err.(*os.SyscallError); ok { if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { brokenPipe = true } } } // 获取堆栈信息 stack := string(debug.Stack()) // 获取原始请求内容 httpRequest, _ := httputil.DumpRequest(c.Request, false) if brokenPipe { slog.ErrorContext(c.Request.Context(), "网络连接中断", slog.Any("error", err), slog.String("request", string(httpRequest)), ) c.Error(err.(error)) c.Abort() return } // 记录 Panic 详情 slog.ErrorContext(c.Request.Context(), "Recovery from panic", slog.Any("error", err), slog.String("stack", stack), slog.String("request", string(httpRequest)), ) ctx := c.Request.Context() traceID, _ := ctx.Value(TraceIDKey).(string) // 返回 500 状态码 c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ "code": http.StatusInternalServerError, "msg": "Internal Server Error", "data": nil, "timestamp": time.Now().Format(time.RFC3339), "trace_id": traceID, }) } }() c.Next() } } func main() { // 初始化 slog baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, }) handler := &ContextHandler{Handler: baseHandler} jsonLogger := slog.New(handler) slog.SetDefault(jsonLogger) // 使用 gin.New() 而不是 gin.Default(),避免内置日志干扰 r := gin.New() r.Use(SlogMiddleware()) r.Use(SlogRecovery()) r.GET("/ping", func(c *gin.Context) { slog.InfoContext(c.Request.Context(), "Processing /ping request", slog.String("user", "zhangsan"), ) time.Sleep(time.Second * 2) c.JSON(200, gin.H{"msg": "pong"}) }) r.GET("/panic", func(c *gin.Context) { slog.InfoContext(c.Request.Context(), "About to panic") panic("something went wrong") }) r.Run(":8080") } 运行后测试: $ curl http://localhost:8080/ping {"msg":"pong"} $ curl http://localhost:8080/panic {"code":500,"msg":"Internal Server Error","data":null,"timestamp":"2026-02-15T14:30:00+08:00","trace_id":"xxx-xxx-xxx"} 日志输出文件 写日志文件一定要注意控制日志文件大小,建议配合系统的logrotate。如果服务运行在kubernetes,建议只输出控制台日志,由专门的日志收集平台去获取控制台日志。 基本实现 写到app.log中 package main import ( "log/slog" "os" ) func main() { logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { panic(err) } handler := slog.NewJSONHandler(logFile, nil) logger := slog.New(handler) slog.SetDefault(logger) slog.Info("hello world") } 配合logrotate。在 /etc/logrotate.d/myapp 创建配置文件 /path/to/app.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate # 复制后截断,不需要重启 Go 程序 } 使用lumberjack轮转日志文件 如果不想用系统的 logrotate ,可以使用 lumberjack 包,它提供了更灵活的日志轮转策略。 import "gopkg.in/natefinch/lumberjack.v2" func initLumberjack() { rollingFile := &lumberjack.Logger{ Filename: "./logs/app.log", MaxSize: 100, // 单位 MB MaxBackups: 3, // 保留旧文件的最大个数 MaxAge: 28, // 保留旧文件的最大天数 Compress: true, // 是否压缩 } handler := slog.NewJSONHandler(rollingFile, nil) slog.SetDefault(slog.New(handler)) } 同时输出控制台和日志文件 go1.26 版本后实现了slog.NewMultiHandler,1.26 前可使用io.multiwriter。 package main import ( "log/slog" "os" ) func main() { logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { panic(err) } fileHandler := slog.NewJSONHandler(logFile, nil) consoleHandler := slog.NewTextHandler(os.Stdout, nil) multiHandler := slog.NewMultiHandler(fileHandler, consoleHandler) // slog.NewMultiHandler 需要go1.26.0+版本 logger := slog.New(multiHandler) slog.SetDefault(logger) slog.Info("hello world") } 自定义日志级别 除了四个内置级别,slog 还支持自定义日志级别 (一般来说默认的日志级别已经够用了): package main import ( "log/slog" "os" ) func main() { // 定义自定义日志级别 const ( LevelTrace = slog.Level(-8) // 比 Debug 更低 LevelNotice = slog.Level(2) // 介于 Info 和 Warn 之间 LevelFatal = slog.Level(12) // 比 Error 更高 ) logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: LevelTrace, // 设置最低级别 })) logger.Log(nil, LevelTrace, "trace message") logger.Log(nil, LevelNotice, "notice message") logger.Log(nil, LevelFatal, "fatal message") } 总结 slog 作为 Go 官方的结构化日志库,用起来还是挺方便的。对于新项目,推荐直接使用 slog;对于已有项目,可以逐步迁移,slog 的 API 设计使得迁移成本很低。