go内置日志库——slog
slog是 Go 1.21 版本正式引入的结构化日志标准库,用于替代传统的log库,核心优势是支持结构化输出(如 JSON、键值对)、日志级别控制、自定义处理器,能更好地适配日志收集、分析的生产级场景
go内置日志库——slog
发布时间:2026-03-10 (2026-03-10)

slog是 Go 1.21 版本正式引入的结构化日志标准库,用于替代传统的log库,核心优势是支持结构化输出(如 JSON、键值对)、日志级别控制、自定义处理器,能更好地适配日志收集、分析的生产级场景。

基本概念

  • 日志级别:内置 4 个级别(从低到高):slog.LevelDebugslog.LevelInfoslog.LevelWarnslog.LevelError,可通过配置控制输出哪些级别。
  • 处理器(Handler):负责日志的格式化和输出,内置两种常用 Handler:
    • slog.TextHandler:输出文本格式(键值对),适合终端查看;
    • slog.JSONHandler:输出 JSON 格式,适合日志平台(如 ELK)解析。
  • Logger:日志记录器,默认使用slog.Default()(基于 TextHandler 输出到标准输出),也可自定义 Logger。

基础使用

适合快速输出日志的场景

package main

import (
  "log/slog"
  "os"
)

func main() {
  textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  textLogger.Debug("你好")
  textLogger.Info("你好")
  textLogger.Warn("你好")
  textLogger.Error("你好")

  jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
  jsonLogger.Debug("你好")
  jsonLogger.Info("你好")
  jsonLogger.Warn("你好")
  jsonLogger.Error("你好")
}

slog.HandlerOptions有三个属性可以改,分别控制是否显示日志行、设置日志级别、替换字段

显示日志行

textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    AddSource: true,
}))

设置日志级别

  textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    Level:     slog.LevelError,
  }))

时间格式化

比较麻烦,需要在ReplaceAttr里面实现

textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    AddSource: true,
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
      if a.Key == slog.TimeKey {
        a.Value = slog.StringValue(a.Value.Time().Format(time.DateTime))
      }
      return a
    },
  }))

日志双写

使用io.MultiWriter

logFile, _ := os.OpenFile(
    filepath.Join(".", "app.log"),
    os.O_CREATE|os.O_WRONLY|os.O_APPEND,
    0644,
  )
  textLogger := slog.New(slog.NewTextHandler(io.MultiWriter(logFile, os.Stdout), &slog.HandlerOptions{
    AddSource: true,
  }))

自定义日志级别

默认的只有 四种, debug,info,warn,error

package main

import (
  "context"
  "log/slog"
  "os"
)

// CustomLevel 1. 定义自定义级别类型(基于slog.Level),解决非本地类型无法重写方法的问题
type CustomLevel slog.Level

// LevelFatal 定义自定义级别常量
const (
  LevelFatal CustomLevel = 12 // 自定义Fatal级别(数值12)
)

// 2. 为自定义级别实现String()方法,返回对应的名称
func (l CustomLevel) String() string {
  switch l {
  case LevelFatal:
    return "FATAL" // 关键:映射为FATAL
  default:
    return slog.Level(l).String() // 其他级别沿用默认
  }
}

func main() {
  textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    AddSource: true,
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
      if a.Key == slog.LevelKey {
        level := CustomLevel(a.Value.Any().(slog.Level))
        a.Value = slog.StringValue(level.String())
      }
      return a
    },
  }))

  textLogger.Debug("你好")
  textLogger.Info("你好")
  textLogger.Warn("你好")
  textLogger.Error("你好")
  textLogger.Log(context.Background(), slog.Level(LevelFatal), "你好")
}

添加固有字段

比如给所有日志添加app_nameenv等固定字段

使用logger对象的With方法

textLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
  AddSource: true,
}))

newLogger := textLogger.With(
  slog.String("appName", "fengfengApp"),
  slog.Int("version", 1),
)

newLogger.Debug("你好")
newLogger.Info("你好")
newLogger.Warn("你好")
newLogger.Error("你好")

也可以用handler的WithAttrs

var handler slog.Handler
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
  AddSource: true,
})
handler = handler.WithAttrs([]slog.Attr{
  slog.String("appName", "fengfengApp"),
  slog.Int("version", 1),
})

textLogger := slog.New(handler)

textLogger.Debug("你好")
textLogger.Info("你好")
textLogger.Warn("你好")
textLogger.Error("你好")

自定义Handler

slog本身没有直接提供「钩子函数(Hook)」的官方 API,但可以通过自定义 Handler实现钩子的核心功能(比如日志输出前 / 后执行自定义逻辑:发送告警、过滤日志、额外字段注入等)

slog 的核心是Handler接口,所有日志最终都会通过Handler.Handle()方法处理。我们只需包装内置 Handler(如 TextHandler/JSONHandler),在Handle()方法中插入自定义钩子逻辑(如前置检查、后置通知)。

比如将error级别的日志单独写入文件

将error单独写入文件

package main

import (
  "context"
  "io"
  "log/slog"
  "os"
  "sync"
)

// MyHandler 自定义日志处理器,实现 slog.Handler 接口
type MyHandler struct {
  slog.Handler                  // 内嵌原有 Handler,实现装饰器模式
  errorFileHandler slog.Handler // 专门处理 Error 级别的 Handler
  once             sync.Once    // 确保错误日志文件只初始化一次
  errFile          *os.File     // 错误日志文件句柄
}

// NewMyHandler 创建自定义日志处理器
// 参数 w: 普通日志的输出目标(如控制台)
// 返回: 自定义的 MyHandler 实例
func NewMyHandler(w io.Writer) *MyHandler {
  return &MyHandler{
    // 初始化普通日志的 TextHandler(带源码信息)
    Handler: slog.NewTextHandler(w, &slog.HandlerOptions{
      AddSource: true,
    }),
  }
}

// initErrorFile 初始化错误日志文件(懒加载,只执行一次)
func (m *MyHandler) initErrorFile() error {
  var err error
  m.once.Do(func() {
    // 打开/创建错误日志文件,追加模式,权限 0644
    m.errFile, err = os.OpenFile(
      "error.log",
      os.O_CREATE|os.O_WRONLY|os.O_APPEND,
      0644,
    )
    if err != nil {
      slog.Error("初始化错误日志文件失败", "err", err)
      return
    }

    // 创建错误日志专用的 TextHandler
    m.errorFileHandler = slog.NewTextHandler(m.errFile, &slog.HandlerOptions{
      AddSource: true, // 同样包含源码信息
    })
  })
  return err
}

// Handle 重写日志处理逻辑
func (m *MyHandler) Handle(ctx context.Context, record slog.Record) error {
  // 如果是 Error 级别日志,单独写入 error.log
  if record.Level == slog.LevelError {
    // 初始化错误日志文件(只执行一次)
    if err := m.initErrorFile(); err != nil {
      return err
    }
    // 将 Error 日志写入文件
    if err := m.errorFileHandler.Handle(ctx, record); err != nil {
      return err
    }
  }

  // 所有级别日志仍输出到原目标(如控制台)
  return m.Handler.Handle(ctx, record)
}

func main() {
  // 创建自定义日志处理器
  myHandler := NewMyHandler(os.Stdout)

  // 创建日志实例
  textLogger := slog.New(myHandler)

  // 测试不同级别日志
  textLogger.Debug("调试信息")
  textLogger.Info("普通信息")
  textLogger.Warn("警告信息")
  textLogger.Error("错误信息", "err_code", 500, "msg", "数据库连接失败")
}

敏感信息脱敏+错误日志自动记录堆栈

有些时候,可能会在日志里面记录用户的敏感信息

这个时候就可以在handler里面进行脱敏操作

package main

import (
  "context"
  "log/slog"
  "os"
  "runtime/debug"
)

// FilterHandler 日志过滤/增强处理器
type FilterHandler struct {
  slog.Handler
}

func NewFilterHandler() *FilterHandler {
  return &FilterHandler{
    Handler: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{}),
  }
}

// Handle 重写日志处理逻辑,修复字段重复问题
func (h *FilterHandler) Handle(ctx context.Context, record slog.Record) error {
  // 1. 定义脱敏规则和字段过滤逻辑
  attrs := make([]slog.Attr, 0, record.NumAttrs())
  record.Attrs(func(a slog.Attr) bool {
    key := a.Key
    val := a.Value

    // 手机号脱敏:替换原有值,而非新增
    if key == "phone" && val.Kind() == slog.KindString {
      phone := val.String()
      if len(phone) == 11 {
        attrs = append(attrs, slog.String("phone", phone[:3]+"****"+phone[7:]))
        return true
      }
    }

    // 密码直接屏蔽
    if key == "password" {
      attrs = append(attrs, slog.String("password", "******"))
      return true
    }

    // 其他字段直接保留
    attrs = append(attrs, a)
    return true
  })

  // 2. 忽略 Debug 级别日志
  if record.Level == slog.LevelDebug {
    return nil
  }

  // 3. 构建新的 Record,替换原有属性
  newRecord := slog.NewRecord(record.Time, record.Level, record.Message, record.PC)
  // 添加脱敏后的属性
  for _, a := range attrs {
    newRecord.AddAttrs(a)
  }

  // 4. Error 级别日志添加堆栈信息
  if record.Level == slog.LevelError {
    newRecord.AddAttrs(slog.String("stack", string(debug.Stack())))
  }

  // 5. 使用新的 Record 调用原始 Handler
  return h.Handler.Handle(ctx, newRecord)
}

func main() {
  handler := NewFilterHandler()
  logger := slog.New(handler)

  logger.Debug("调试信息(会被忽略)")
  logger.Info("用户注册", "phone", "13812345678", "password", "123456")
  logger.Error("支付失败", "order_id", 9999)
}

日志轮转

日志轮转(Log Rotation)是生产环境中必备的日志管理手段,用于避免单个日志文件过大、方便日志归档 / 清理。Go 的slog本身不内置日志轮转功能,但可以通过结合第三方库(如rotatelogs)实现,这也是 Go 生态中最成熟、最常用的方案。

package main

import (
  "fmt"
  rotatelogs "github.com/lestrrat-go/file-rotatelogs"
  "log/slog"
  "time"
)

func main() {

  writer, err := rotatelogs.New(
    "logs/app-%Y%m%d.log", // 按日期进行日志文件命名
    //rotatelogs.WithLinkName("app.log"),        // 创建一个指向最新日志文件的软链接
    rotatelogs.WithMaxAge(7*24*time.Hour),     // 文件最大保存时间
    rotatelogs.WithRotationTime(24*time.Hour), // 每天进行日志切换
    //rotatelogs.WithRotationCount(10),          // 日志文件最大保存个数
    rotatelogs.WithLocation(time.Local), // 本地时间
    rotatelogs.ForceNewFile(),              // 重新打开文件
    rotatelogs.WithClock(rotatelogs.Local), // 本地时间
    //rotatelogs.WithHandler(&MyHandler{}),   //事件处理
  )
  if err != nil {
    fmt.Println(err)
  }

  handler := slog.NewTextHandler(writer, nil)
  logger := slog.New(handler)

  logger.Debug("调试信息(会被忽略)")
  logger.Info("用户注册", "phone", "13812345678", "password", "123456")
  logger.Error("支付失败", "order_id", 9999)
}