0%

go 库学习之 slog 库

上一篇文章学习了 go 标准库的 log 包和 log/syslog 包,log 包提供了简单的日志记录功能,而 log/syslog 则提供了将日志记录到系统 syslog 服务的能力。这篇文章将继续学习标准库的 log/slog 库,log/slog 库提供了结构化日志功能,它所记录的日志包含了消息、日志级别以及通过键值对所表示的各种属性。

属性

首先通过如下示例直观地感受下什么叫做结构化日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"log/slog"
"os"
)

func main() {
tl := slog.New(slog.NewTextHandler(os.Stderr, nil))
gl := slog.New(slog.NewJSONHandler(os.Stderr, nil))

tl.Info("request done", slog.Int("id", 100), slog.String("os", "Windows"))
gl.Info("request done", slog.Int("id", 100), slog.String("os", "Windows"))
}
1
2
3
# go run slog.go
time=2024-12-15T17:17:05.096+08:00 level=INFO msg="request done" id=100 os=Windows
{"time":"2024-12-15T17:17:05.09655238+08:00","level":"INFO","msg":"request done","id":100,"os":"Windows"}

在该示例中,创建一个了 TextHandlerJSONHandlerLogger 对象,分别用它们输出两条相同内容的日志:

  • 对于 TextHandler,它以 key=value 的形式输出日志中的各个属性,这些属性包括 timelevelmsgidos
  • 对于 JSONHandler,它以 JSON 格式输出日志内容,各个属性就是 JSON 对象中的 键值对

可以看到,结构化的日志的关键就是认为日志是由一系列的 属性 构成的,而属性就是通过 键值对 表示的。键永远都是字符串类型,而值可以是多种数据类型。

日志中的 属性 可以是 slog 内建的属性,也可以是用户自定义的属性。如下就是 slog 所有内建属性的 key:

1
2
3
4
5
6
7
// Keys for "built-in" attributes.
const (
TimeKey = "time"
LevelKey = "level"
MessageKey = "msg"
SourceKey = "source"
)

这就是为什么上面示例的日志输出中包含 timelevelmsg 等属性,只要创建的 Logger 对象开启了这些内建属性对应的开关,那么输出的日志内容里会自动包含相应的内建属性。

Attr 类型

结构化日志的核心就是 属性,一条日志其实就是由任意个属性组成的。上面的实例中,我们通过 slog.Int()slog.String() 等函数为日志内容添加自定义属性。slog.Attr 类型用来表示日志属性,它的定义如下:

1
2
3
4
5
// An Attr is a key-value pair.
type Attr struct {
Key string
Value Value
}

Attr 的定义也可以看出,属性的确就是 键值对,其中 Key 代表属性的名称,它总是字符串类型,而 Value 则代表属性的值,它的类型为 Value

Value 类型

Value 用来表示属性中的值,该类型定义如下:

1
2
3
4
5
type Value struct {
_ [0]func() // disallow ==
num uint64
any any
}

Value 可以表示任何 Go 类型的值,但是不同于直接使用 any 类型,Value 类型会尽量尝试使用其 num 字段来保存某些预定义类型的值(也称为 small value),从而避免内存分配。只有当的确无法使用 num 来保存该类型的值时,才会直接使用 any 接口来保存该类型的值。使用 any 接口类型的确可以保存任意类型的值,但是这种赋值会触发内存申请来保存值的副本(即接口的装箱操作)。

slog 库提供了以下函数来创建 Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 可以为任意类型的值生成 Value
func AnyValue(v any) Value

// 为 bool 类型的值生成 Value
func BoolValue(v bool) Value
// 为 time.Duration 类型的值生成 Value
func DurationValue(v time.Duration) Value
// 为 float64 类型的值生成 Value
func Float64Value(v float64) Value
// 为 int64 类型的值生成 Value
func Int64Value(v int64) Value
// 为 int 类型的值生成 Value
func IntValue(v int) Value
// 为字符串类型的值生成 Value
func StringValue(value string) Value
// 为 time.Time 类型的值生成 Value
func TimeValue(v time.Time) Value
// 为 uint64 类型的值生成 Value
func Uint64Value(v uint64) Value

// 为一系列 Attr 生成一个 Group Value
func GroupValue(as ...Attr) Value

这些函数中最特殊的就是 GroupValue,它允许将一系列 Attr([]Attr)直接打包成一个 Group Value。另外,slog 库也提供了对应的方法来获取 Value 所表示的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 以 any 接口类型返回 Value 所表示的值
func (v Value) Any() any

// 以 bool 类型返回 Value 所表示的值
func (v Value) Bool() bool
// 以 time.Duration 类型返回 Value 所表示的值
func (v Value) Duration() time.Duration
// 以 float64 类型返回 Value 所表示的值
func (v Value) Float64() float64
// 以 int64 类型返回 Value 所表示的值
func (v Value) Int64() int64
// 返回 Value 的字符串表示
// 如果 Value 本身就保存的就是字符串,那么以 string 类型返回 Value 所表示的值
func (v Value) String() string
// 以 time.Time 类型返回 Value 所表示的值
func (v Value) Time() time.Time
// 以 uint64 类型返回 Value 所表示的值
func (v Value) Uint64() uint64

// 返回 `Group value` 所保存的一系列 Attr
func (v Value) Group() []Attr

另外,slog 库提供了一个接口类型 LogValuer,任何类型都可以通过实现该接口类型来定义如何将自身转换为 Value

1
2
3
type LogValuer interface {
LogValue() Value
}

Value 也提供了如下方法来操作 LogValuer 类型的 Value:

1
2
3
4
5
// 以 LogValuer 类型返回 Value 所表示的值
func (v Value) LogValuer() LogValuer

// 如果 v 实现了 `LogValuer`,那么会调用它的 `LogValue` 来获取 v 对应的 Value,该过程会一直递归进行
func (v Value) Resolve() (rv Value)

Value.Kind() 方法可以返回 Value 的种类:

1
func (v Value) Kind() Kind

下面的实例展示了 value 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"log/slog"
)

type Token string

func (Token) LogValue() slog.Value {
return slog.StringValue("******")
}

type A struct {
a int
}

func main() {
v1 := slog.Int64Value(100)
v2 := slog.AnyValue(Token("secret"))
v3 := slog.AnyValue(A{})

// Int64 100
fmt.Println(v1.Kind(), v1.Int64())
// LogValuer ******
fmt.Println(v2.Kind(), v2.Resolve().String())
// Any {0}
fmt.Println(v3.Kind(), v3.Any().(A))
}

Attr 的创建

在介绍完 Value 类型后,我们再来看 Attr 的创建。slog 库提供了一系列工具函数来简化 Attr 的创建。我们首先看 Any 函数的实现,它可以为任意类型的值生成 Attr

1
2
3
4
// 就是通过 AnyValue 函数来为任意执行的值创建 Attr 中的 Value
func Any(key string, value any) Attr {
return Attr{key, AnyValue(value)}
}

了解了 Any 函数的实现后,其他 Attr 创建函数都是类似的,这里简单列举如下:

1
2
3
4
5
6
7
8
9
func Bool(key string, v bool) Attr
func Duration(key string, v time.Duration) Attr
func Float64(key string, v float64) Attr
func Int(key string, value int) Attr
func Int64(key string, value int64) Attr
func String(key, value string) Attr
func Time(key string, v time.Time) Attr
func Uint64(key string, v uint64) Attr
func Group(key string, args ...any) Attr

Group Attr

上面的 Attr 创建函数中有一个特别的 Group 函数,它用于创建一个 Group Attr,即 key 是字符串,但是 value 是一组属性(即上文所说的 Group Value)。该函数的第一个参数就是 Group Attr 的 key,而之后的 args 参数会按照每两个一组的方式来创建一个 Attr,所有的 args 就可以转换为一组 Attr,从而创建 Group Attr 出的 Value。

Group Attr 可以将一组属性进行分组,而 Group Attr 的 key 则会限定这组属性的名称,至于这个 限定 是如何显示的,则取决于所使用的 Handler:

  • 对于 TextHandler,会以 GroupKey.AttrKey=AttrValue 的方式显示 Group 中的某个属性
  • 对于 JSONHandler,则会将 Group 作为单独一个 JSON 对象,Group Attr 的 Key 就是该 JSON 对象的 Key,例如 "GroupKey": { "AttrKey": AttrValue }

如下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"log/slog"
"os"
)

func main() {
tl := slog.New(slog.NewTextHandler(os.Stderr, nil))
gl := slog.New(slog.NewJSONHandler(os.Stderr, nil))

// time=2024-12-15T21:25:33.764+08:00 level=INFO msg="request done" request.id=100 request.os=Windows
tl.Info("request done", slog.Group("request", "id", 100, "os", "Windows"))
// {"time":"2024-12-15T21:25:33.765114567+08:00","level":"INFO","msg":"request done","request":{"id":100,"os":"Windows"}}
gl.Info("request done", slog.Group("request", "id", 100, "os", "Windows"))
}

记录日志

为了使用 slog 库记录日志,首先需要有一个 slog.Logger 类型的对象。slog.Logger 类型定义如下:

1
2
3
type Logger struct {
handler Handler // for structured logging
}

可以看到,slog.Logger 类型只包含一个 Handler 类型的成员 handler,它决定了处理每一条日志记录的具体行为。通过如下函数来基于某个 Handler 创建 slog.Logger 对象:

1
func New(h Handler) *Logger

Handler

Handler 是一个接口类型,它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
type Handler interface {
// Enabled 方法用来表示该 handler 是否处理该级别的日志
Enabled(context.Context, Level) bool

// Handle 用来真正处理日志,只有 Enabled 返回 true,才会处理该日志
Handle(context.Context, Record) error

// 创建一个新的 Handler,该 Handler 新包含指定的 Attrs
WithAttrs(attrs []Attr) Handler

// 创建一个新的 Handler,它为 Handler 打开一个 Group
WithGroup(name string) Handler
}

HandlerHandle 方法用来真正处理日志,日志记录通过 Record 类型来表示。虽然 slog 提供了多种参数形式的 API 来处理日志,但是其内部最终还是使用 Record 类型来表示 一条日志。它的定义如下:

1
2
3
4
5
6
7
8
9
10
type Record struct {
// 日志产生的时间
Time time.Time
// 日志消息内容
Message string
// 该日志记录的级别
Level Level
// program counter,用来计算产生日志的源文件/代码行号信息
PC uintptr
}

slog 包内置了两种 Handler 的实现,即 TextHandlerJSONHandler,分别以普通文本格式和 JSON 格式来输出日志。它们的定义如下:

1
2
3
4
5
6
7
type TextHandler struct {
*commonHandler
}

type JSONHandler struct {
*commonHandler
}

可以看到,这两种类型都是通过内嵌 *commonHandler 来实现的,因为 TextHandlerJSONHandler 只是最终的日志输出形式不一样,其对结构化日志 AttrGroup 等特性的处理都是类似的,因此 slog 包通过提供 commonHandler 类型,用来实现结构化日志的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type commonHandler struct {
// 是否是 json 输出
// 对于 JSONHandler,该字段为 true
// 对于 TextHandler,该字段为 false
json bool
// Handler 选项
opts HandlerOptions
// 在 handler 级别已经预定义好的属性
preformattedAttrs []byte
// 当前的 group 名称前缀
groupPrefix string
// 使用 WithGroup 添加的 Group 名称
groups []string
// 已经打开的 groups 个数
nOpenGroups int
// 互斥锁,支持并发日志
mu *sync.Mutex
// 日志的输出目的地
w io.Writer
}

TextHandlerJSONHandler 的创建函数如下:

1
2
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler
func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler

这两个函数都支持 HandlerOptions 参数,可以控制 Handler 的某些行为。如果 opts 参数为空,会使用 HandlerOptions 的默认值:

1
2
3
4
5
6
7
8
9
type HandlerOptions struct {
// 是否在输出的日志中添加源文件/源代码行号信息
AddSource bool
// 日志级别控制
// 如果没有指定,默认日志级别为 LevelInfo
Level Leveler
// 对日志中的 Attr 进行替换
ReplaceAttr func(groups []string, a Attr) Attr
}

HandlerOptionsLevel 字段决定了该 Handler 所处理的最小日志级别,它的类型是一个接口类型:

1
2
3
type Leveler interface {
Level() Level
}

处理每一条日志时都会调用 Leveler.Level() 方法,将结果与当前日志记录的级别进行判断,从而决定当前日志是否需要处理,这样就为日志输出提供了动态控制能力。

WithAttrs/WithGroup

有时候,某些 Attr、Group 对所有日志都是通用的。对于这些公共 Attr、Group,如果每次记录日志时都显式添加/设置的话,会显得非常冗余,因此 Handler 定义了如下两个方法,支持直接在 Handler 级别上添加公共 Attr、打开 Group:

1
2
3
4
5
// 创建一个新的 Handler,该 Handler 新包含指定的 Attrs
WithAttrs(attrs []Attr) Handler

// 创建一个新的 Handler,它为 Handler 打开一个 Group
WithGroup(name string) Handler

如下是一个简单示例,可以帮助理解这两个函数的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"log/slog"
"os"
)

func main() {
tl := slog.New(slog.NewTextHandler(os.Stdout, nil))

// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" id=100
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" id=200
tl.Info("text log", "id", 100)
tl.Info("text log", "id", 200)

tl1 := slog.New(
tl.Handler().WithAttrs([]slog.Attr{
slog.String("os", "linux"),
}),
)
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" os=linux id=100
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" os=linux id=200
tl1.Info("text log", "id", 100)
tl1.Info("text log", "id", 200)

tl2 := slog.New(
tl.Handler().WithGroup("request"),
)
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" request.id=100
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" request.id=200
tl2.Info("text log", "id", 100)
tl2.Info("text log", "id", 200)

tl3 := slog.New(
tl.Handler().
WithAttrs([]slog.Attr{slog.String("os", "linux")}).
WithGroup("request"),
)
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" os=linux request.id=100
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" os=linux request.id=200
tl3.Info("text log", "id", 100)
tl3.Info("text log", "id", 200)

tl4 := slog.New(
tl.Handler().
WithGroup("request").
WithAttrs([]slog.Attr{slog.String("os", "linux")}),
)
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" request.os=linux request.id=1000
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" request.os=linux request.id=200
// time=2024-12-16T17:15:37.556+08:00 level=INFO msg="text log" request.os=linux request.subrequest.subid=100
tl4.Info("text log", "id", 100)
tl4.Info("text log", "id", 200)
tl4.Info("text log", slog.Group("subrequest", "subid", "100"))
}

WithGroup 函数会在 Handler 上打开一个 Group,打开的 Group 会对后续的 Attr/Group 生效。后续的 Attr/Group 既可以是在 Handler 上添加的 Attr/Group,也可以是来自日志记录本身的 Attr/Group。

无论是在 Handler 上添加 Attr/Group,还是后面处理日志本身的 Attr/Groupslog 库都是借助 handleState 来实现日志内容的输出控制的。理解了 handleState 的相关逻辑,就理解了 slog 库的核心原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type handleState struct {
// 所属的 handler
h *commonHandler
// 要输出的内容,日志的构造就是通过写入该字段实现的
buf *buffer.Buffer
// 当释放 handleState 时是否需要释放 buf
// 因为 buf 并不总是新申请的,所以通过该字段判断是否真的要释放 buf
freeBuf bool
// 写入下一个 key 前的分隔符
sep string
// 当写入 Attr 时,Attr.key 前缀
// 一般是 group 名称
prefix *buffer.Buffer
// 记录所有打开的 group,为 ReplaceAttr 提供 groups 参数
groups *[]string
}

(*commonHandler).withAttrs() 以及 (*commonHandler).handle() 方法都会借助 handleState 来实现日志输出内容的控制,只不过 withAttrs 是在 Handler 级别上控制日志的输出,而 handle 则是在 日志记录 级别上控制日志的输出。

1
2
3
4
5
6
7
// 在 Handler 上添加 Attrs
// 其使用 handleState 来管理 handler 级别上日志输出行为
func (h *commonHandler) withAttrs(as []Attr) *commonHandler

// 处理每一条日志记录
// 其使用 handleState 来管理日志记录级别上的日志输出行为
func (h *commonHandler) handle(r Record) error

slog.Logger 的 API

上面的分析就是 slog 库的底层实现原理,接下来快速列举一下 slog.Logger 所提供的 API,通过这些 API,来实现日志的记录功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 基于 Handler 创建一个 Logger
func New(h Handler) *Logger

// 返回 Logger 所使用的 Handler
func (l *Logger) Handler() Handler

// 自动将参数按照解析为一组 Attr 并将这些 Attr 添加到 Logger 的 Handler 上
// 解析时将每两个参数解析为一个 Attr(但是参数本身就是 Attr 类型,则直接接受该参数)
func (l *Logger) With(args ...any) *Logger
// 为 Logger 的 Handler 打开一个 Group
func (l *Logger) WithGroup(name string) *Logger

// 判断是否需要处理 level 级别的日志
func (l *Logger) Enabled(ctx context.Context, level Level) bool

// 记录一条日志,其中 args 会被解析为该日志记录的一组 Attr,解析规则如上所属
func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any)
// 记录一条日志,attrs 参数直接就是日志的 Attr
func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {

// 记录 Debug 级别的日志
func (l *Logger) Debug(msg string, args ...any)
func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)

// 记录 Info 级别的日志
func (l *Logger) Info(msg string, args ...any)
func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)

// 记录 Warn 级别的日志
func (l *Logger) Warn(msg string, args ...any)
func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any)

// 记录 Error 级别的日志
func (l *Logger) Error(msg string, args ...any)
func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any)

这些日志记录函数都提供了 XXXContext 版本,方便 Handler 从 context.Context 中获取信息。但是内置的 TextHanlerJSONHandler 本身都忽略了 ctx 参数,所以如果要用上该参数,可能需要自己重新实现 Handler(可以基于已有的 TextHandler 或者 JSONHandler 实现):

1
2
3
4
5
6
7
func (h *TextHandler) Handle(_ context.Context, r Record) error {
return h.commonHandler.handle(r)
}

func (h *JSONHandler) Handle(_ context.Context, r Record) error {
return h.commonHandler.handle(r)
}

默认 Logger 对象

同样 slog 库也提供了一个预定义的、默认的 Logger 对象,slog 库的 Info、Error 等顶级函数就是调用这个预定义 Logger 对象的相应方法来实现的。这样就简化了 slog 库的使用,省去了手动创建 Logger 对象的步骤。

这个预定义的 logger 对象是在 slog 库的 init 函数创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
var defaultLogger atomic.Pointer[Logger]

func init() {
defaultLogger.Store(New(newDefaultHandler(loginternal.DefaultOutput)))
}

func newDefaultHandler(output func(uintptr, []byte) error) *defaultHandler {
return &defaultHandler{
// 初始化 commonHandler
ch: &commonHandler{json: false},
output: output,
}
}

默认的 logger 对象是基于 defaultHandler 来实现的,它的核心还是基于 commonHandler 来实现的日志 Attr/Group 的处理,唯一需要注意的是它的 output 函数是调用 log 库的 std.output 函数来实现的,即调用的是 log 库默认 logger 对象的输出函数:

1
2
3
4
5
6
7
8
9
10
11
// go/src/log/log.go

func init() {
// 设置 internal.DefaultOutput
internal.DefaultOutput = func(pc uintptr, data []byte) error {
// 调用 log 包预定义的 logger 对象 std 的 output 函数来输出日志
return std.output(pc, 0, func(buf []byte) []byte {
return append(buf, data...)
})
}
}

因此,虽然 slogdefaultLogger 是基于 commonHandler (非 json 格式)来实现的,但是其输出格式还是与 TextHandler 有所差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"log/slog"
"os"
)

func main() {
tl := slog.New(slog.NewTextHandler(os.Stderr, nil))
// time=2024-12-16T22:24:02.384+08:00 level=INFO msg="log message" id=100
tl.Info("log message", "id", 100)

// 2024/12/16 22:24:02 INFO log message id=100
slog.Info("log message", "id", 100)
}

slog 库也提供了一些与默认 logger 对象相关的 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 返回 slog 默认的 default Logger 对象
func Default() *Logger

// 设置 slog 默认的 default Logger 对象
// 需要注意的是,不仅仅 slog 的 Info/Error 等顶级函数会使用新的 logger 对象输出日志
// log 包 default logger 对象的日志输出函数内部也是调用该新的 logger 对象的 Handler 函数来处理其要输出的日志
func SetDefault(l *Logger)

// slog 默认 default Logger 对象的日志输出函数
func Log(ctx context.Context, level Level, msg string, args ...any)
func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)
func Debug(msg string, args ...any)
func DebugContext(ctx context.Context, msg string, args ...any)
func Info(msg string, args ...any)
func InfoContext(ctx context.Context, msg string, args ...any)
func Warn(msg string, args ...any)
func WarnContext(ctx context.Context, msg string, args ...any)
func Error(msg string, args ...any)
func ErrorContext(ctx context.Context, msg string, args ...any)

slog 库桥接到 log 库

如果我们的应用程序代码已经是基于 log 库的 API 来输出日志了,那么怎么能让其使用上 slog 库提供的结构化日志能力呢?slog 库提供了 NewLogLogger 函数来实现这个需求:

1
func NewLogLogger(h Handler, level Level) *log.Logger

NewLogLogger 函数内部实现是通过将用户提供的 Handler 封装为一个 handlerWriter 来实现的,handlerWriter 实现了 io.Writer 接口,这样 log 库的 logger 对象在输出日志时,就会调用 handlerWriter 的 Write 方法来输出日志,而 handlerWriter 内部则继续调用 Handler 的 Handle 方法来处理该日志:

1
2
3
4
5
6
7
8
9
func NewLogLogger(h Handler, level Level) *log.Logger {
return log.New(&handlerWriter{h, level, true}, "", 0)
}

type handlerWriter struct {
h Handler
level Leveler
capturePC bool
}

如下是一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

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

func main() {
// 2024/12/17 11:34:47 hello
log.Println("hello")
ll := slog.NewLogLogger(slog.NewTextHandler(os.Stdout, nil), slog.LevelInfo)
// time=2024-12-17T11:34:47.864+08:00 level=INFO msg=hello
ll.Println("hello")
}

小结

这篇文章详细介绍了 slog 库的使用方法和基本原理,重点介绍了其 Attr、Handler 等核心数据类型,方便我们理解 slog 库的核心设计思路。slog 库提供了结构化日志的核心抽象(Attr/Group),内置了 TextHandler 和 JSONHandler 两种日志处理器,同时也允许我们通过自定义 Handler 来扩展 slog 的能力。