Go - slog使用入门

简介

slog 是 Go 1.21 引入的官方结构化日志库(Structured Logging)。它结束了 Go 标准库只有简单 log 包的历史,让我们可以直接输出 JSONKey-Value 格式的日志,非常适合对接 ELK、Grafana Loki 等日志分析系统。

相较于第三方日志库如 zaplogrusslog 的优势在于:

  • 零依赖:作为标准库的一部分,无需引入第三方依赖
  • 官方维护:长期稳定,API 变更有 Go 兼容性承诺保障
  • 接口简洁:API 设计清晰,学习成本低
  • 可扩展:通过自定义 Handler 可以实现各种定制需求

基本使用

slog 用起来非常简单。默认输出到标准错误流(os.Stderr),格式为普通文本。

go 复制代码
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))
}

运行输出:

shell 复制代码
$ 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:

go 复制代码
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))
}

运行输出:

shell 复制代码
$ 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 添加通用属性。这些属性会自动附加到每条日志记录中,适合注入服务名、环境、版本等上下文信息。

go 复制代码
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")
}

运行输出:

shell 复制代码
$ 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 将相关属性组织在一起,使输出结构更清晰:

go 复制代码
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"),
		),
	)
}

运行输出:

shell 复制代码
$ 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),避免了反射带来的性能开销。

go 复制代码
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)),
		)
	}
}

运行输出:

shell 复制代码
$ 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 提供了 InfoContextWarnContext 等方法,可以从 context.Context 中提取数据。默认情况下,这些方法不会自动提取 context 中的值,需要通过自定义 Handler 来实现。

自定义 ContextHandler

以下示例实现了一个自定义 Handler,用于从 context 中提取 TraceID:

go 复制代码
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")
}

运行输出:

shell 复制代码
$ 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 来提取它。

go 复制代码
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")
}

运行后测试:

shell 复制代码
$ 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

go 复制代码
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 包,它提供了更灵活的日志轮转策略。

go 复制代码
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

go 复制代码
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 还支持自定义日志级别 (一般来说默认的日志级别已经够用了):

go 复制代码
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 设计使得迁移成本很低。

相关推荐
u***35742 小时前
对基因列表中批量的基因进行GO和KEGG注释
开发语言·数据库·golang
好家伙VCC6 小时前
# 发散创新:基于 Go 语言打造高性能服务网格的实践与突破在微服务架构
java·python·微服务·架构·golang
unirst19850077 小时前
搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
开发语言·后端·golang
YGGP7 小时前
【Golang】LeetCode 3. 无重复字符的最长子串
开发语言·leetcode·golang
大黄说说8 小时前
Go 实战 LeetCode 151:高效翻转字符串中的单词(含空格处理技巧)
开发语言·leetcode·golang
风无雨8 小时前
Go 本地启动踩坑:为什么 `go run main.go` 会报 `undefined`?
golang
桂花很香,旭很美9 小时前
[7天实战入门Go语言后端] Day 0:预习——关键词、语法与常用包一览
开发语言·后端·golang
桂花很香,旭很美9 小时前
[7天实战入门Go语言后端] Day 1:Go 基础入门——环境、语法、错误处理与并发
开发语言·后端·golang
女王大人万岁20 小时前
Golang标准库 CGO 介绍与使用指南
服务器·开发语言·后端·golang