在 Go 1.21 之前,一直没有一个标准的结构化日志包,其默认的 log 包只有简单的记录功能,不能满足结构化和工程化的需要。在此背景下,出现许多优秀的日志框架,如 zap、zerolog 等, 包括许多 Web 框架也集成了其私有的日志包。这样带来的问题就是没有类似于 Java 中 slf4j 那样的统一日志前端,如果发现使用的日志框架无法满足新功能的需求,那么需要对代码中所有的日志输出调用进行重写,非常麻烦。在 Go 1.21 之后,Go 官方终于推出了其结构化的日志包 slog,具备简单的日志结构化输出功能,但是 Go 官方更多是想让 slog 做为一个统一的日志前端,通过实现其 Handler 来适配不同的后端,包括但不限于 zap、zerolog 等。
简单使用
slog 包提供了 slog.Info、slog.Warn、slog.Error、slog.Debug 进行简单的日志结构化输出
slog.Info("hello world")
slog.Error("hello world")
其默认的输出为
2025/07/01 17:56:22 INFO hello world
2025/07/01 17:56:22 ERROR hello world
slog 并没有提供类似于 Infof 这样的方法用于做字符串格式化,而是推荐使用 key、value 的形式将需要记录的数据单独输出,这样的好处就是参数输出可以更加直观并且在使用 Json 格式进行日志输出时,可以更加的结构化。
slog.Info("hello world", slog.String("name", "xin"),
slog.Int64("age", 1),
slog.Bool("is_student", false),
slog.Any("roles", []string{"admin", "user"}))
其输出内容如下
2025/07/01 18:00:35 INFO hello world name=xin age=1 is_student=false roles="[admin user]"
此外,也支持使用 Group 对参数进行分组,方便更加的结构化。
slog.Info("hello world", slog.Group("user", slog.String("name", "1"), slog.Int("gender", 1)))
slog.Group 第一个参数为 Group 的名称,后续的参数都为 key、value 的形式,其输出也更加直观。
2025/07/02 10:15:24 INFO hello world user.name=1 user.gender=1
携带固定参数
slog 提供了 With 函数用于添加自定义的参数,在某个 Logger 对象中调用了 With 函数之后,会返回一个新的 Logger 对象,被称作为子 Logger,其参数会一直存在于这个子 Logger 中,同时,也会附带父 Logger 中所有的固定参数,这在一些场景下非常有用,例如 Web 项目中的链路追踪。
s1 := slog.Default().With(slog.String("requestId", "1111"))
s1.Info("hello world")
s2 := s1.With(slog.String("userId", "1"))
s2.Info("hello world")
子 Logger 中打印的日志会带上其自有的和父 Logger 中的所有固定参数
2025/07/02 10:26:28 INFO hello world requestId=1111
2025/07/02 10:26:28 INFO hello world requestId=1111 userId=1
如果不想使用这种子 Logger 的形式去传递参数,在 Go 中这种实现需要 Logger 对象从上到下依次传递,会与业务逻辑造成一定的耦合。那么也可以通过更加通用的 Context 对象和 slog.xxxxContext 函数来从 Context 中获取指定的固定参数并输出。
// 定义个处理Context的Handler
type ContextHandler struct {
slog.Handler
keys []string // 需要从 Context 中读取的字段
}
func NewContextHandler(parent slog.Handler, keys []string) slog.Handler {
return &ContextHandler{
keys: keys,
Handler: parent,
}
}
func (self *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
for _, key := range self.keys {
value := ctx.Value(key)
if value == nil {
continue
}
// 适配各种可能的value类型
switch v := value.(type) {
case string:
r.AddAttrs(slog.String(key, v))
case int64:
r.AddAttrs(slog.Int64(key, v))
case uint64:
r.AddAttrs(slog.Uint64(key, v))
case bool:
r.AddAttrs(slog.Bool(key, v))
default:
r.AddAttrs(slog.Any(key, v))
}
}
return self.Handler.Handle(ctx, r)
}
func main() {
parent := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: false,
Level: slog.LevelInfo,
})
logger := slog.New(NewContextHandler(parent, []string{"requestId"}))
slog.SetDefault(logger)
ctx := context.WithValue(context.Background(), "requestId", "11111")
slog.InfoContext(ctx, "hello world")
}
只需要传入需要从 Context 中读取的 keys,那么就可以在传递 Context 时自动读取参数
time=2025-07-02T13:18:56.077+08:00 level=INFO msg="hello world" requestId=11111
简单的格式自定义
在一些场景下,可能需要自定义日志时间、定义日志字段的间隔或者带上日志的 source 等等自定义的需求,在 slog 中,可以通过自定义其 Handler 实现。
// 简单的使用 replace 实现,但是不能自定义细节
options := &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
switch attr.Key {
case slog.TimeKey:
time := attr.Value.Time()
attr.Value = slog.StringValue(time.Format("2006-02-01 15:04:05"))
}
return attr
},
}
handler := slog.NewTextHandler(os.Stdout, options)
logger := slog.New(handler)
logger.Info("hello world")
其输出格式符合 Json 的风格
time="2025-01-07 18:12:01" level=INFO source=/home/cola/Documents/Code/Goland/learning/main.go:23 msg="hello world"
通过自定义 Handler 可以实现更加精细的控制
// 自定义的 slog.Handler
type CustomHandler struct {
w io.Writer
level slog.Level
}
func NewCustomHandler(w io.Writer, level slog.Level) *CustomHandler {
return &CustomHandler{
w: w,
level: level,
}
}
func (h *CustomHandler) Enabled(_ context.Context, level slog.Level) bool {
return level >= h.level
}
func (h *CustomHandler) Handle(_ context.Context, r slog.Record) error {
ts := r.Time.Format("2006-01-02 15:04:05") // 自定义时间格式
level := r.Level.String()
msg := r.Message
// 输出时间、级别、消息
fmt.Fprintf(h.w, "[%s] [%s] %s", ts, level, msg)
internalKey := map[string]struct{}{
slog.TimeKey: {},
slog.LevelKey: {},
slog.MessageKey: {},
slog.SourceKey: {},
}
r.Attrs(func(attr slog.Attr) bool {
if _, ok := internalKey[attr.Key]; ok {
fmt.Fprintf(h.w, " %v", attr.Value)
} else {
fmt.Fprintf(h.w, " %s=%v", attr.Key, attr.Value)
}
return true
})
fmt.Fprintln(h.w)
return nil
}
func (h *CustomHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return h // 可拓展支持 context attr}
func (h *CustomHandler) WithGroup(_ string) slog.Handler {
return h // 分组暂不处理
}
// 使用自定义的 handler
handler := NewCustomHandler(os.Stdout, slog.LevelInfo)
logger := slog.New(handler)
logger.Info("hello world", slog.Any("names", []string{"1", "2"}))
其输出格式如下
[2025-07-01 18:20:07] [INFO] hello world names=[1 2]
其它日志格式
slog 默认的日志格式为文本格式,对于开发环境比较友好,但是对于测试和线上环境来说,其不利用做日志收集和日志可视化。所有,在项目中通常会配置 Text 和 Json 两个日志格式化器,分别用于本地开发和其他环境。
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
})
logger := slog.New(handler)
// 将其设置为全局logger,后续可以直接通过 slog.xxx 调用
slog.SetDefault(logger)
slog.Info("hello world", slog.String("name", "xin"), slog.Any("names", []string{"1", "2"}))
其输出为 Json 格式
{
"time": "2025-07-01T18:27:12.702428062+08:00",
"level": "INFO",
"source": {
"function": "main.main",
"file": "/home/cola/Documents/Code/Goland/learning/main.go",
"line": 15
},
"msg": "hello world",
"name": "xin",
"names": [
"1",
"2"
]
}
多个输出
slog 默认只会输出到 os.Stdout 控制台中,对于本地开发来说是够了,但是在其它环境中,为了方便的查询日志和收集日志,通常需要将日志输出到文件和日志收集平台或 MQ 中。slog 也可以方便的集成这些功能,只需要实现 io.Writer 接口即可。
file, err := os.Open("app.log")
if err != nil && !os.IsExist(err) {
file, _ = os.Create("app.log")
}
defer file.Close()
handler := slog.NewTextHandler(io.MultiWriter(os.Stdout, file), &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
})
logger := slog.New(handler)
slog.SetDefault(logger)
slog.Info("hello world", slog.String("name", "xin"), slog.Any("names", []string{"1", "2"}))
此时会将日志同时输出到控制台和指定的日志文件中
使用其它后端
如果需要结构化日志也就是输出为 Json 格式,推荐使用 zerolog 后端,如果是在本地开发环境,需要美观和更好可读性,可以使用 zap 的 console 后端。
// zap适配slog的Handler
func newZapHandler() slog.Handler {
encode := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
stdout := zapcore.AddSync(os.Stdout)
core := zapcore.NewCore(encode, stdout, zapcore.InfoLevel)
return zapslog.NewHandler(core)
}
// zarolog 适配slog的Handler
func newZerologHandler() slog.Handler {
// TODO
panic("TODO")
}
func main() {
// 可以使用各种适配的日志后端
logger := slog.New(newZapHandler())
slog.SetDefault(logger)
slog.Info("hello world", slog.String("name", "name"))
}
完整配置
在开发环境下使用 Console 后端以获得更好的可读性,而在其它环境使用 Json 后端来更方便做日志采集和分析,这是目前日志工程化中的常用做法,此外还可以搭配其它日志轮转和切割等功能。
// zap适配slog的Handler
func newZapHandler(env string, writers ...io.Writer) (slog.Handler, func()) {
var encode zapcore.Encoder
// dev模式使用Console, 否则都使用Json
if env == "dev" {
encode = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
} else {
encode = zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
// 添加所有传递的writer
syncWriters := make([]zapcore.WriteSyncer, 0)
for _, wr := range writers {
syncWriters = append(syncWriters, zapcore.AddSync(wr))
}
stdout := zapcore.AddSync(zapcore.NewMultiWriteSyncer(syncWriters...))
core := zapcore.NewCore(encode, stdout, zapcore.InfoLevel)
// 返回闭包函数,用于调用所有writer的sync函数进行持久化
return zapslog.NewHandler(core), func() {
for _, wr := range syncWriters {
wr.Sync()
}
}
}
// 创建日志轮转和切割的Writer
func newLumberjackLogger(logFilePath string) (io.Writer, error) {
if err := os.MkdirAll(logFilePath, 0o777); err != nil {
return nil, err
}
filename := time.Now().Format("2006-01-02") + ".log"
fullFilename := path.Join(logFilePath, filename)
if _, err := os.Stat(fullFilename); err != nil {
if _, err := os.Create(fullFilename); err != nil {
return nil, err
}
}
return &lumberjack.Logger{
Filename: fullFilename,
MaxSize: 20,
MaxBackups: 5,
MaxAge: 10,
Compress: true,
}, nil
}
func main() {
file, _ := newLumberjackLogger("./logs/")
handler, done := newZapHandler("dev", os.Stdout, file)
// 主函数执行完成调用所有日志输出的Sync()函数进行强制刷写
defer done()
logger := slog.New(handler)
slog.SetDefault(logger)
slog.Info("hello world", slog.String("name", "name"))
}
更新:slog 包在最新的 benchmark 中性能已经和 zap 等广泛使用的日志库没有太大差距,完全可以不使用其它其它日志后端了。
但是 slog 默认的 TextHandler 在开发环境下几乎没有可读性,为了在开发时更加的方法,便封装了一个支持颜色显示的 ConsoleHandler ,以下是在生产环境中使用的详细配置。
首先是 ContextHandler ,用于自动解析 context 中包含的 key,方便链路追踪。
const (
contextKey = "context" // 强烈建议将ContextKey的类型改为结构体,不然会被覆盖
)
type contextHandler struct {
slog.Handler
keys []string
}
func newContextHandler(parent slog.Handler, keys ...string) slog.Handler {
return &contextHandler{
keys: keys,
Handler: parent,
}
}
func (self *contextHandler) Handle(ctx context.Context, record slog.Record) error {
if self.keys == nil || len(self.keys) == 0 {
return self.Handler.Handle(ctx, record)
}
attrs := make([]slog.Attr, 0, len(self.keys))
for _, key := range self.keys {
value := ctx.Value(key)
if value == nil {
continue
}
// 适配各种可能的value类型
switch v := value.(type) {
case string:
attrs = append(attrs, slog.String(key, v))
case int64:
attrs = append(attrs, slog.Int64(key, v))
case uint64:
attrs = append(attrs, slog.Uint64(key, v))
case bool:
attrs = append(attrs, slog.Bool(key, v))
case time.Duration:
attrs = append(attrs, slog.Duration(key, v))
case time.Time:
attrs = append(attrs, slog.Time(key, v))
default:
attrs = append(attrs, slog.Any(key, v))
}
}
record.AddAttrs(slog.Any(contextKey, slog.GroupValue(attrs...)))
return self.Handler.Handle(ctx, record)
}
然后是 ConsoleHandler 实现,用于日志的拼接和颜色格式化
const (
reset = "\033[0m"
black = "30"
red = "31"
green = "32"
yellow = "33"
blue = "34"
magenta = "35"
cyan = "36"
lightGray = "37"
darkGray = "90"
lightRed = "91"
lightGreen = "92"
lightYellow = "93"
lightBlue = "94"
lightMagenta = "95"
lightCyan = "96"
white = "97"
)
type consoleHandler struct {
slog.Handler
pool *sync.Pool // 使用对象池重复使用buffer
wr io.Writer
mutex *sync.Mutex
}
func newConsoleHandler(parent slog.Handler, wr io.Writer) slog.Handler {
return &consoleHandler{
Handler: parent,
pool: &sync.Pool{
New: func() any {
return &bytes.Buffer{}
},
},
wr: wr,
mutex: &sync.Mutex{},
}
}
func (self *consoleHandler) Handle(_ context.Context, record slog.Record) error {
builder := self.pool.Get().(*bytes.Buffer)
// 格式化时间
self.colorize(builder, lightGray, record.Time.Format("2006-01-02 15:04:05.000"))
builder.WriteByte(' ')
// 根据日志的 Level 格式化为不同的颜色
switch record.Level {
case slog.LevelDebug:
self.colorize(builder, lightGray, record.Level.String())
case slog.LevelInfo:
self.colorize(builder, cyan, record.Level.String())
case slog.LevelWarn:
self.colorize(builder, lightYellow, record.Level.String())
case slog.LevelError:
self.colorize(builder, lightRed, record.Level.String())
default:
builder.WriteString(record.Level.String())
}
builder.WriteByte(' ')
// 如果开启了 source 通过runtime获取打印日志的函数和行数
if config.ViperGet[bool]("logger.source", false) {
sr := source(&record)
_, file := path.Split(sr.Function)
self.colorize(builder, lightBlue, file+":"+strconv.Itoa(sr.Line))
builder.WriteString(" - ")
}
// 对打印的消息进行颜色格式化
self.colorize(builder, green, record.Message)
record.Attrs(func(attr slog.Attr) bool {
switch attr.Key {
case slog.TimeKey, slog.LevelKey, slog.MessageKey:
return true
default:
builder.WriteByte(' ')
builder.WriteString(attr.String())
}
return true
})
builder.WriteByte('\n')
self.mutex.Lock()
// 回收buffer 释放锁
defer func() {
self.mutex.Unlock()
builder.Reset()
self.pool.Put(builder)
}()
_, err := self.wr.Write(builder.Bytes())
return err
}
func source(record *slog.Record) *slog.Source {
fs := runtime.CallersFrames([]uintptr{record.PC})
f, _ := fs.Next()
return &slog.Source{
Function: f.Function,
File: f.File,
Line: f.Line,
}
}
// 文字染色
func (self *consoleHandler) colorize(buffer *bytes.Buffer, colorCode, value string) {
buffer.WriteString("\033[")
buffer.WriteString(colorCode)
buffer.WriteByte('m')
buffer.WriteString(value)
buffer.WriteString(reset)
}
最后是 logger 实现,在开发环境中使用 ConsoleHandler ,线上环境和测试环境使用 JsonHandler 。
// 用于日志配置和slog日志级别的映射
var loggerLevelMap = map[string]slog.Level{
"DEBUG": slog.LevelDebug,
"INFO": slog.LevelInfo,
"WARN": slog.LevelWarn,
"ERROR": slog.LevelError,
}
func NewLogger() (*slog.Logger, error) {
env := config.ViperGet[string]("server.environment", "dev")
var handler slog.Handler
levelStr := strings.ToUpper(config.ViperGet[string]("logger.level", "INFO"))
level, ok := loggerLevelMap[levelStr]
if !ok {
level = slog.LevelInfo
}
options := &slog.HandlerOptions{
Level: level,
AddSource: config.ViperGet[bool]("logger.source", false),
}
// 开发环境使用ConsoleHandler 只输出到控制台
// 其它环境使用JsonHandler 输出到控制台和文件中
if env == "dev" {
handler = newConsoleHandler(slog.NewTextHandler(io.Discard, options), os.Stdout)
} else {
fileWrite, err := newLumberjackLogger()
if err != nil {
return nil, err
}
handler = slog.NewJSONHandler(io.MultiWriter(os.Stdout, fileWrite), options)
}
return slog.New(handler), nil
}
func NewLoggerWithContext(keys ...string) (*slog.Logger, error) {
logger, err := NewLogger()
if err != nil {
return nil, err
}
handler := newContextHandler(logger.Handler(), keys...)
return slog.New(handler), nil
}
// 输出日志到文件,并做日志切割
func newLumberjackLogger() (io.Writer, error) {
fileDir := config.ViperGet[string]("logger.file-path", "./logs/")
if err := os.MkdirAll(fileDir, 0o777); err != nil {
return nil, err
}
filename := time.Now().Format("2006-01-02") + ".log"
fullFilename := path.Join(fileDir, filename)
if _, err := os.Stat(fullFilename); err != nil {
if _, err := os.Create(fullFilename); err != nil {
return nil, err
}
}
return &lumberjack.Logger{
Filename: fullFilename,
MaxSize: config.ViperGet[int]("logger.max-size", 50),
MaxBackups: config.ViperGet[int]("logger.max-backups", 5),
MaxAge: config.ViperGet[int]("logger.max-age", 10),
Compress: config.ViperGet[bool]("logger.compress", false),
}, nil
}
如果还需要添加一些其它的日志输出,例如 MQ 的 Kafka 等,也是只需要添加 Writer 即可。
最后进行 Benchmark 测试,不输出,只测试其解析日志的性能。
func BenchmarkConsoleHandler(b *testing.B) {
handler := newConsoleHandler(nil, io.Discard) // 不输出
rec := slog.NewRecord(time.Now(), slog.LevelInfo, "message", 0)
rec.AddAttrs(slog.String("user", "alice"), slog.Int("id", 42))
for i := 0; i < b.N; i++ {
_ = handler.Handle(context.Background(), rec)
}
}
func BenchmarkContextHandlerAndConsole(b *testing.B) {
handler := newContextHandler(newConsoleHandler(nil, io.Discard), "traceId")
rec := slog.NewRecord(time.Now(), slog.LevelInfo, "message", 0)
rec.AddAttrs(slog.String("user", "alice"), slog.Int("id", 42))
ctx := context.WithValue(context.Background(), "traceId", "231231")
for i := 0; i < b.N; i++ {
_ = handler.Handle(ctx, rec)
}
}
func BenchmarkContextHandlerAndJSON(b *testing.B) {
handler := newContextHandler(slog.NewJSONHandler(io.Discard, nil), "traceId")
rec := slog.NewRecord(time.Now(), slog.LevelInfo, "message", 0)
rec.AddAttrs(slog.String("user", "alice"), slog.Int("id", 42))
ctx := context.WithValue(context.Background(), "traceId", "231231")
for i := 0; i < b.N; i++ {
_ = handler.Handle(ctx, rec)
}
}
测试结果如下,其性能完全可以满足本地开发和生产环境的需求。
❯ go test -bench=. -benchmem -cpu=1,2,4,8,16
goos: linux
goarch: amd64
pkg: github.com/wnnce/fserv-template/logging
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkConsoleHandler 1515848 706.3 ns/op 120 B/op 6 allocs/op
BenchmarkConsoleHandler-2 1680400 692.4 ns/op 120 B/op 6 allocs/op
BenchmarkConsoleHandler-4 1690282 709.7 ns/op 120 B/op 6 allocs/op
BenchmarkConsoleHandler-8 1687108 716.2 ns/op 120 B/op 6 allocs/op
BenchmarkConsoleHandler-16 1700002 699.3 ns/op 120 B/op 6 allocs/op
BenchmarkContextHandlerAndConsole 898359 1251 ns/op 328 B/op 14 allocs/op
BenchmarkContextHandlerAndConsole-2 1018027 1195 ns/op 328 B/op 14 allocs/op
BenchmarkContextHandlerAndConsole-4 943408 1184 ns/op 328 B/op 14 allocs/op
BenchmarkContextHandlerAndConsole-8 903339 1214 ns/op 328 B/op 14 allocs/op
BenchmarkContextHandlerAndConsole-16 942187 1204 ns/op 328 B/op 14 allocs/op
BenchmarkContextHandlerAndJSON 1887498 650.4 ns/op 88 B/op 3 allocs/op
BenchmarkContextHandlerAndJSON-2 2035083 595.8 ns/op 88 B/op 3 allocs/op
BenchmarkContextHandlerAndJSON-4 1997014 595.6 ns/op 88 B/op 3 allocs/op
BenchmarkContextHandlerAndJSON-8 1982528 614.4 ns/op 88 B/op 3 allocs/op
BenchmarkContextHandlerAndJSON-16 1978699 603.3 ns/op 88 B/op 3 allocs/op






0 条评论