slog是 Go 1.21 版本正式引入的结构化日志标准库,用于替代传统的log库,核心优势是支持结构化输出(如 JSON、键值对)、日志级别控制、自定义处理器,能更好地适配日志收集、分析的生产级场景。
基本概念
- 日志级别:内置 4 个级别(从低到高):
slog.LevelDebug、slog.LevelInfo、slog.LevelWarn、slog.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_name、env等固定字段
使用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)
}