Go 日志系统:log 与 log/slog 完全指南
目录
备忘用系统性笔记。忘了直接翻这一篇。
Go 标准库有两个日志相关的包:
log:Go 1.0 就有,简单、无级别、纯文本。log/slog:Go 1.21 引入,结构化日志,带级别、Handler、上下文。
一句话区分:log 适合小工具/起步,slog 适合要采集、要过滤、要结构化字段的服务。
一、标准库 log
1.1 零配置即用
1import "log"
2
3func main() {
4 log.Print("hello") // 2026/10/05 17:00:00 hello
5 log.Printf("port=%d", 8080) // 2026/10/05 17:00:00 port=8080
6 log.Println("a", "b") // 2026/10/05 17:00:00 a b
7}默认输出到 stderr,默认前缀是日期+时间(LstdFlags = Ldate | Ltime)。
1.2 会退出的两个函数
1log.Fatal("boom") // 打印后调用 os.Exit(1)
2log.Fatalf("boom %v", err)
3log.Panic("boom") // 打印后 panic注意:Fatal 内部是 os.Exit,不会执行 defer。想知道区别可以记:
| 函数 | 行为 |
|---|---|
Print* | 只打印 |
Panic* | 打印 + panic(会走 defer,可被 recover) |
Fatal* | 打印 + os.Exit(1)(不走 defer) |
1.3 配置输出目标 / 前缀 / 格式
1log.SetOutput(os.Stdout) // 改输出流
2log.SetPrefix("[tabapp] ") // 前缀
3log.SetFlags(log.LstdFlags | log.Lshortfile) // 日期时间 + 短文件名:行号flag 可组合:
| flag | 含义 |
|---|---|
log.Ldate | 2026/10/05 |
log.Ltime | 17:00:00 |
log.Lmicroseconds | 17:00:00.000000 |
log.Llongfile | 完整路径 + 行号 |
log.Lshortfile | 文件名 + 行号 |
log.LUTC | 时间用 UTC(默认本地时区) |
log.Lmsgprefix | 把前缀放到消息前,而非行首 |
log.LstdFlags | 等于 `Ldate |
1.4 自定义 Logger(多实例)
1logger := log.New(os.Stdout, "API: ", log.LstdFlags|log.Lshortfile)
2logger.Printf("request %s", r.URL.Path)log.New(out io.Writer, prefix string, flag int) *log.Logger。这样每个模块可以有独立的输出流和前缀。
1.5 log 的局限
- 没有日志级别:只能靠字符串前缀
[ERROR]自己约定,无法按级别过滤。 - 非结构化:只能拼字符串,机器难解析。
- 没有公共字段:每条日志都要重复拼
service=xxx。 - 没有上下文:无法把
request_id、trace_id自动带上。
这些正是 slog 要解决的。
二、log/slog
2.1 设计模型(最重要的图)
slog 分四层:
1 ┌────────────┐
2调用 → │ Logger │ 接收调用,组装 Record(时间、Level、msg、Attrs)
3 └─────┬──────┘
4 ▼
5 ┌────────────┐
6 │ Handler │ 决定“怎么输出/写到哪/要不要输出”(级别过滤在此)
7 └─────┬──────┘
8 ▼
9 ┌────────────┐
10 │ Record │ 一次日志的数据
11 └─────┬──────┘
12 ▼
13 ┌────────────┐
14 │ Attr │ 键值对
15 └────────────┘- Logger:日志入口。
- Handler:真正干活的地方,负责格式化、过滤、写出。内置
TextHandler和JSONHandler,也可以自定义。 - Record:一条日志记录。
- Attr:一个键值对字段。
关键点:slog.Info(...) 这类包级函数用的是全局默认 Logger,级别过滤发生在 Handler。
2.2 快速上手
1import "log/slog"
2
3func main() {
4 // 1. 选一个 Handler + 输出目标
5 handler := slog.NewJSONHandler(os.Stdout, nil)
6 // 2. 包装成 Logger
7 logger := slog.New(handler)
8 // 3. 设为全局默认(之后 slog.Info 等就用它)
9 slog.SetDefault(logger)
10
11 slog.Info("配置加载成功", "app_name", "tabapp", "port", 8080)
12}输出:
1{"time":"2026-10-05T17:00:00.000+08:00","level":"INFO","msg":"配置加载成功","app_name":"tabapp","port":8080}字段说明:time / level / msg 是 Handler 自动加的,其余是你传的键值对。
2.3 级别(Level)
1slog.Debug("debug") // -4
2slog.Info("info") // 0
3slog.Warn("warn") // 4
4slog.Error("error") // 8底层就是 int,越大越严重。级别最核心的作用是过滤:
1record.Level >= 阈值 → 输出
2record.Level < 阈值 → 整条丢弃(连字段都不会出现)阈值默认是 Info:所以默认情况下 Debug 日志完全不输出。
2.4 结构化字段
1slog.Info("login",
2 "user", "alice",
3 "id", 42,
4 "ok", true,
5 "elapsed", 120*time.Millisecond,
6)规则:
- 第一个参数是
msg。 - 后面是成对的 key/value,数量必须为偶数。
- 若某个参数本身是
slog.Attr,会直接使用。 - key 不是字符串或参数不成对时,slog 会产生
!BADKEY。
也可以用强类型构造避免拼写/成对错误:
1slog.LogAttrs(ctx, slog.LevelInfo, "login",
2 slog.String("user", "alice"),
3 slog.Int("id", 42),
4 slog.Bool("ok", true),
5 slog.Duration("elapsed", 120*time.Millisecond),
6)slog.Attr 构造函数一览:
| 构造函数 | 类型 |
|---|---|
slog.String(key, string) | 字符串 |
slog.Int(key, int) / slog.Int64 | 整数 |
slog.Uint64(key, uint64) | 无符号 |
slog.Float64(key, float64) | 浮点 |
slog.Bool(key, bool) | 布尔 |
slog.Time(key, time.Time) | 时间 |
slog.Duration(key, time.Duration) | 时长 |
slog.Any(key, v) | 任意值 |
slog.Group(key, args...) | 嵌套分组 |
2.5 公共字段:With / WithGroup
每条日志重复写 service、request_id 很烦,用 With 一次性绑定:
1logger := slog.Default().With(
2 "service", "tabapp",
3 "version", "1.0.0",
4)
5logger.Info("started") // 自动带 service/version
6logger.Warn("slow request") // 自动带 service/versionWithGroup 把字段收进一个命名空间:
1logger.WithGroup("http").Info("request", "method", "GET", "path", "/health")Text 输出约为 http.method=GET http.path=/health;JSON 输出为嵌套对象 {"http":{"method":"GET","path":"/health"}}。
2.6 Handler:Text 还是 JSON
1// 人类友好,开发环境用
2slog.NewTextHandler(os.Stderr, nil)
3// 输出:time=2026-10-05T17:00:00.000+08:00 level=INFO msg=started port=8080
4
5// 机器友好,生产/采集用
6slog.NewJSONHandler(os.Stdout, nil)
7// 输出:{"time":"...","level":"INFO","msg":"started","port":8080}2.7 HandlerOptions
1&slog.HandlerOptions{
2 AddSource: true, // 加 source(文件:行号),有性能开销
3 Level: slog.LevelDebug, // 过滤阈值
4 ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
5 // 每条属性都会回调,可改 key/value,或返回零值删除
6 return a
7 },
8}Level 字段
类型是接口 slog.Leveler,凡是实现了 Level() Level 的都能传。常用两种:
1Level: slog.LevelWarn // 固定值
2Level: levelVar // *slog.LevelVar,可运行时改AddSource
1AddSource: true
2// JSON: {"source":{"function":"main.main","file":"/app/main.go","line":42}}开启后能定位日志来源,但会增加开销(每次要取调用栈),生产环境按需开。
ReplaceAttr(万能出口)
内置字段 time / level / msg / source 都会经过这个回调,可用来改格式或删字段:
1ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
2 // 只处理顶层
3 if len(groups) > 0 {
4 return a
5 }
6 switch a.Key {
7 case slog.TimeKey:
8 // 时间转毫秒 + UTC
9 return slog.String(slog.TimeKey,
10 a.Value.Time().UTC().Format("2006-01-02T15:04:05.000Z07:00"))
11 case slog.LevelKey:
12 // 级别改小写
13 return slog.String(slog.LevelKey,
14 strings.ToLower(a.Value.String()))
15 }
16 return a
17}删除某个字段就返回零值 slog.Attr{}。
2.8 LevelVar:动态调级
级别常常要等读完配置才知道,LevelVar 支持运行时修改,且并发安全:
1level := new(slog.LevelVar)
2level.Set(slog.LevelInfo)
3
4slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
5 Level: level,
6})))
7
8// 之后任意时刻可改,立即生效
9level.Set(slog.LevelDebug)典型用法:先默认 Info,配置里 mode: debug 时降到 Debug。
2.9 带 context 的版本
有 ctx 时用带 Context 的函数(便于接入 trace):
1slog.InfoContext(ctx, "handled", "path", r.URL.Path)
2slog.ErrorContext(ctx, "failed", "err", err)
3slog.LogAttrs(ctx, slog.LevelWarn, "slow", slog.Duration("took", d))注意:内置 Handler 不会自动读取 ctx 里的值,需要自定义 Handler 才能把 trace_id 之类提取出来。
2.10 LogAttrs:性能优先
LogAttrs 只接受 slog.Attr,避免了 ...any 装箱,热路径推荐:
1logger.LogAttrs(ctx, slog.LevelInfo, "query",
2 slog.String("sql", stmt),
3 slog.Int("rows", n),
4)配合 Enabled 避免昂贵参数计算:
1if logger.Enabled(ctx, slog.LevelDebug) {
2 logger.Debug("dump", "data", expensiveDump())
3}2.11 与 log 互操作
把 log 的输出接到 slog
1slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
2slog.SetLogLoggerLevel(slog.LevelDebug) // log.Print 会以 Debug 级走 slog handler调用 SetDefault 后,log.Print 等默认输出会转交给 slog 的 Handler;SetLogLoggerLevel 控制这些输出的级别。
让 slog 输出成一个 *log.Logger
1handler := slog.NewJSONHandler(os.Stdout, nil)
2stdLogger := slog.NewLogLogger(handler, slog.LevelInfo)
3stdLogger.Printf("via log") // 走 slog handler2.12 丢弃日志 / 多路输出
1// 完全丢弃(测试或临时静音)
2slog.SetDefault(slog.New(slog.DiscardHandler))
3
4// 同时写文件和控制台(新版 Go)
5h := slog.NewMultiHandler(
6 slog.NewJSONHandler(file, nil),
7 slog.NewTextHandler(os.Stderr, nil),
8)
9slog.SetDefault(slog.New(h))2.13 自定义 Handler(了解即可)
想要特殊行为(脱敏、分流告警、注入 trace)时,实现接口:
1type Handler interface {
2 Enabled(context.Context, Level) bool
3 Handle(context.Context, Record) error
4 WithAttrs(attrs []Attr) Handler
5 WithGroup(name string) Handler
6}通常更省事的做法是「包一层」内置 Handler,或在 ReplaceAttr 里做字段级处理。
三、log vs slog 对比
| 维度 | log | log/slog |
|---|---|---|
| 引入版本 | 1.0 | 1.21 |
| 输出形态 | 纯文本 | 结构化(Text / JSON) |
| 日志级别 | 无 | Debug/Info/Warn/Error |
| 级别过滤 | 不支持 | 支持(Handler 阈值) |
| 公共字段 | 手动拼 | With / WithGroup |
| 上下文 | 无 | *Context / LogAttrs |
| 可插拔 | 仅换 io.Writer | Handler 接口,可自定义 |
| Fatal | 有 | 无(需自己 Error + os.Exit) |
| 适用 | 小工具、脚本 | 服务、需要采集/过滤 |
选型建议:
- 学习、一次性脚本、极简工具 →
log足够。 - 线上服务、要接 ELK/Loki、要按级别过滤、要结构化查询 →
slog。 - 已有大量
log代码不想全改 → 用SetDefault+SetLogLoggerLevel让log转发到slog。
四、实战:本项目 main.go 的 slog 配置
1func main() {
2 // 结构化日志统一走 JSON,便于采集(stdout)。
3 // level 用 LevelVar,方便读到配置后按 mode 动态调整。
4 level := new(slog.LevelVar)
5 level.Set(slog.LevelInfo)
6 slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
7 Level: level,
8 })))
9
10 if err := run(level); err != nil {
11 // slog 没有内置 Fatal,这里手动 Error + os.Exit。
12 slog.Error("启动失败", "err", err)
13 os.Exit(1)
14 }
15}
16
17func run(level *slog.LevelVar) error {
18 // ... 解析 flag / 查找配置 ...
19
20 cfg, err := config.Load(path)
21 if err != nil {
22 return err
23 }
24
25 if cfg.Server.Mode == "debug" {
26 level.Set(slog.LevelDebug) // 动态降到 Debug
27 }
28
29 slog.Info("配置加载成功",
30 "app_name", cfg.Server.AppName,
31 "port", cfg.Server.Port,
32 "mode", cfg.Server.Mode,
33 "source", path,
34 )
35 return nil
36}要点:
LevelVar用指针,run里改的是 handler 持有的同一个对象。SetDefault让slog.Info等包级函数生效。slog没有Fatal,用run() error+os.Exit(1)统一处理。- 用了
os.Stdout而非stderr,便于容器采集;如需改回只需换参数。
五、常见坑
slog.New后忘记slog.SetDefault:slog.Info仍用默认 logger,你的 Handler 不生效。要么SetDefault,要么直接用返回的logger.Info(...)。- 级别比较方向记反:是
>=阈值才输出。LevelWarn时Warn本身会输出。 - Fatal 不存在:
slog.Fatal会编译不过,需slog.Error(...)后os.Exit(1)。 os.Exit跳过 defer:清理逻辑要放在os.Exit之前,或封装到run()里。- 键值参数不成对:输出里会出现
!BADKEY;用LogAttrs+ 强类型 Attr 可避免。 AddSource有性能开销:生产环境按需开启。- 内置 Handler 不读取 ctx 的值:要注入 trace_id 必须自定义 Handler。
ReplaceAttr忘记判断len(groups) == 0:会把嵌套分组里同名字段一起改掉。log.Fatal与slog混用:转发后log的默认 flag 会被清空(SetDefault内部会log.SetFlags(0)),别再依赖它的时间戳格式。- 输出流变化:
log默认写stderr,JSON handler 常写stdout,部署时注意 stdout/stderr 分流策略。
六、API 速查表
log
1log.Print / Printf / Println
2log.Fatal / Fatalf / Fatalln // 打印后 os.Exit(1)
3log.Panic / Panicf / Panicln // 打印后 panic
4log.Default() *Logger
5log.SetOutput(io.Writer)
6log.SetPrefix(string)
7log.SetFlags(int)
8log.New(out, prefix, flag) *Logger
9// flags: Ldate Ltime Lmicroseconds Llongfile Lshortfile LUTC Lmsgprefix LstdFlagslog/slog:创建与全局
1slog.New(handler) *Logger
2slog.NewTextHandler(w, opts) *TextHandler
3slog.NewJSONHandler(w, opts) *JSONHandler
4slog.NewMultiHandler(...Handler) *MultiHandler // 新版
5slog.SetDefault(*Logger)
6slog.Default() *Logger
7slog.SetLogLoggerLevel(Level) Level
8slog.NewLogLogger(Handler, Level) *log.Logger
9slog.DiscardHandler // 丢弃log/slog:记录日志
1slog.Debug / Info / Warn / Error(msg, args...)
2slog.DebugContext / InfoContext / WarnContext / ErrorContext(ctx, msg, args...)
3slog.Log(ctx, level, msg, args...)
4slog.LogAttrs(ctx, level, msg, attrs...)
5
6logger.Enabled(ctx, level) bool
7logger.With(args...) *Logger
8logger.WithGroup(name) *Logger
9logger.Handler() Handlerlog/slog:类型与常量
1slog.LevelDebug = -4
2slog.LevelInfo = 0
3slog.LevelWarn = 4
4slog.LevelError = 8
5slog.LevelVar{} // Set / Level / String,并发安全
6
7slog.TimeKey / LevelKey / MessageKey / SourceKey
8
9slog.String / Int / Int64 / Uint64 / Float64 / Bool / Time / Duration / Any / GroupHandler 与选项
1type Handler interface {
2 Enabled(context.Context, Level) bool
3 Handle(context.Context, Record) error
4 WithAttrs([]Attr) Handler
5 WithGroup(string) Handler
6}
7
8slog.HandlerOptions{
9 AddSource: bool,
10 Level: slog.Leveler,
11 ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr,
12}七、一句话总结
log:简单,够用,但无级别、非结构化。slog:结构化 + 级别过滤 + Handler 可插拔 + 上下文,是服务端日志的现代标准做法。- 级别首先决定「日志存不存在」,其次才决定「长什么样」。
- 记住四件套:
Logger/Handler/Record/Attr,其余都是围绕它们展开。