0

0

slog:Go官方的结构化日志包开发的怎么样了?该如何使用?

Golang菜鸟

Golang菜鸟

发布时间:2023-08-04 17:01:02

|

1491人浏览过

|

来源于Golang菜鸟

转载

熟悉 Go 的同学都知道 Go 语言标准库 log 有许多痛点,比如没有日志分级、没有结构化(没有 JSON 格式)、扩展性差等,为了解决这些问题 Go 官方推出了结构化日志包 slog,目前这个库正在开发阶段,已经进入了实验库:golang.org/x/exp/slog,目前版本是 v0.0.0。

这篇文章我们就来看下 slog 包怎么用?

安装

使用下面的命令安装:

go get golang.org/x/exp/slog

开箱即用

func main() {
 slog.Info("Go is best language!", "公众号", "Golang来啦")
}

输出:

2023/01/23 10:23:37 INFO Go is best language! 公众号=Golang来啦

看输出有点类似标准库 log 的输出。slog 库里一个非常重要结构体就是 Logger,通过它就可以调用日志记录函数 Info()、Debug() 等。这个我们没有创建 Logger,会使用默认的,大家可以点进去看下源码。

Handler

Handler 定义成一个接口,这可以让 slog 的扩展性更强,slog 提供了两个内置的 Handler 实现:TextHandler 和 JSONHandler,另外我们可以基于第三方 log 包定义或者自己定义 Handler 的实现,这个我们后面会讲到。

type Handler interface {
 Enabled(Level) bool
 Handle(r Record) error
 WithAttrs(attrs []Attr) Handler
 WithGroup(name string) Handler
}

Text Handler

TextHandler 会像标准库 log 包那样将日志以一行文本那样输出。

func main() {
 textHandler := slog.NewTextHandler(os.Stdout)
 logger := slog.New(textHandler)
 logger.Info("Go is best language!", "公众号", "Golang来啦")
}

输出:

time=2023-01-23T10:48:41.365+08:00 level=INFO msg="Go is best language!" 公众号=Golang来啦

我们看到,输出的日志以“key1=value1 key2=value2 … keyN=valueN”形式呈现。

JSON Handler

我们将上面的 NewTextHandler() 换成 NewJSONHandler()

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 logger.Info("Go is best language!", "公众号", "Golang来啦")
}

输出:

{"time":"2023-01-23T11:02:27.1606485+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}

从输出可以看到,日志已 json 格式记录,这样的结构化日志非常适合机器解析。

日志选项

日常开发中我们一般都会在日志里面记录在哪个文件哪一行记录了这条日志,这样有利于排查问题。或者,有时候需要更改日志级别,那这些该怎么实现呢?

如果我们翻看源码就能发现,上面提到的 TextHandler 和 JSONHandler 都使用默认的 HandlerOptions,它是一个结构体。

type HandlerOptions struct {
 AddSource bool
 Level Leveler
 ReplaceAttr func(groups []string, a Attr) Attr
}

通过 slog 的源代码注释可以看出,如果 AddSource 设置为 true,则记录日志时会以 ("source", "file:line") 的方式记录来源;Level 用于调整日志级别。

默认情况下,slog 只会记录 Info 及以上级别的日志,不会记录 Debug 级别的日志。

func main() {
 logger := slog.New(slog.NewJSONHandler(os.Stdout))
 logger.Debug("记录日志-debug",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
 logger.Info("记录日志-info",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}

输出:

{"time":"2023-01-23T15:36:14.8610328+08:00","level":"INFO","msg":"记录日志-info","公众号":"Golang来啦","time":0}

这样的话,我们就可以自定义 option。

func main() {
 opt := slog.HandlerOptions{   // 自定义option
  AddSource: true,
  Level:     slog.LevelDebug,   // slog 默认日志级别是 info
 }

 logger := slog.New(opt.NewJSONHandler(os.Stdout))
 logger.Debug("记录日志-debug",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
 logger.Info("记录日志-info",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}

输出:

{"time":"2023-01-23T15:38:45.3747228+08:00","level":"DEBUG","source":"D:/examples/context/demo1/demo1.go:81","msg":"记录日志-debug","公众号":"Golang来啦","time":0}
{"time":"2023-01-23T15:38:45.3949544+08:00","level":"INFO","source":"D:/examples/context/demo1/demo1.go:84","msg":"记录日志-info","公众号":"Golang来啦","time":0}

从输出可以看到记录日志的时候显示了来源,同时也记录了 debug 级别的日志。

SetDefault() 设置默认 Logger

有一点值得注意的是,slog.SetDefault() 会将传进来的 logger 作为默认的 Logger,所以下面这两行输出是一样的:

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Go is best language!", "公众号", "Golang来啦")
 slog.Info("Go is best language!", "公众号", "Golang来啦")
}

输出:

{"time":"2023-01-23T11:17:32.7518696+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}
{"time":"2023-01-23T11:17:32.7732035+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦"}

另外,如果设置里默认的 Logger,调用 log 包方法时也会使用默认的:

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 log.Print("something went wrong")
 log.Fatalln("something went wrong")
}

输出:

{"time":"2023-01-23T11:18:31.5850509+08:00","level":"INFO","msg":"something went wrong"}
{"time":"2023-01-23T11:18:31.6043829+08:00","level":"INFO","msg":"something went wrong"}
exit status 1

两种记录日志的方式

通过 slog 包记录日志除了上面提到的这种方式:

logger.Info("Go is best language!", "公众号", "Golang来啦")

这种方式会涉及到额外的内存分配,主要是为了简介设计的。

另外一种记录日志方式就像下面这样:

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"))

这两种输出日志格式都是一样的,第二种为了提高记录日志的性能而设计的,需要自己指定日志级别、参数属性(以键值对的方式指定)。

目前 slog 包支持下面这些属性:

String
Int64
Int
Uint64
Float64
Bool
Time
Duration

我们还可以多指定一些属性:

logger.LogAttrs(slog.LevelInfo, "Go is best language!", slog.String("公众号", "Golang来啦"), slog.Int("age", 18))

输出:

{"time":"2023-01-23T11:45:11.7921124124+08:00","level":"INFO","msg":"Go is best language!","公众号":"Golang来啦","age":18}

如何绑定一组属性

学到这里我就在想,假如我想在一个 key 下面绑定一组 key-value 值该怎么做呢?这种需求在日常开发中是很常见的,我翻了翻源码,slog 还真的提供了相关方法 -- slog.Group()。

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout)
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
  slog.String("app-version", "v0.0.0"),
 )
}

输出:

{"time":"2023-01-23T13:45:26.9179901+08:00","level":"INFO","msg":"Usage Statistics","memory":{"current":50,"min":20,"max":80},"cpu":10,"app-version":"v0.0.0"}

memory 元素下面对应不同的 key-value。

如何绑定公共的属性

日常开发中,可能会遇到每一条日志需要记录一些相同的公共信息,比如 app-version。

...

logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
  slog.String("app-version", "v0.0.0"),
 )
 logger.Info("记录日志",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()), slog.String("app-version", "v0.0.0"))

...

如果想上面这样,每次都记录一次 app-version 的话就有点繁琐了。好在 slog 自带的 TextHandler 和 JSONHandler 提供了 WithAttrs() 方法可以实现绑定公共属性。

func main() {
 textHandler := slog.NewJSONHandler(os.Stdout).WithAttrs([]slog.Attr{slog.String("app-version", "v0.0.0")})
 logger := slog.New(textHandler)
 slog.SetDefault(logger)

 logger.Info("Usage Statistics",
  slog.Group("memory",
   slog.Int("current", 50),
   slog.Int("min", 20),
   slog.Int("max", 80)),
  slog.Int("cpu", 10),
 )
 logger.Info("记录日志",
  "公众号", "Golang来啦",
  "time", time.Since(time.Now()))
}

输出:

{"time":"2023-01-23T14:01:46.2845325+08:00","level":"INFO","msg":"Usage Statistics","app-version":"v0.0.0","memory":{"current":50,"min":20,"max":80},"cpu":10}
{"time":"2023-01-23T14:01:46.303597+08:00","level":"INFO","msg":"记录日志","app-version":"v0.0.0","公众号":"Golang来啦","time":0}

从输出可以看到两条日志都记录了 app-version,这种记录方式就简洁多了。

通过 context 存储或提取 Logger

slog 的 Logger 还与 context.Context 结合在一起,比如通过 slog.WithContext() 存储 Logger、通过 slog.FromContext() 提取 Logger。这样我们就可以在不同函数之间通过 context 传递 Logger。

func main() {
 logger := slog.New(slog.NewJSONHandler(os.Stdout))
 http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
  l := logger.With("path", r.URL.Path).With("user-agent", r.UserAgent()) // With() 绑定额外的信息

  ctx := slog.NewContext(r.Context(), l) // 生成 context

  handleRequest(w, r.WithContext(ctx))
 })

 http.ListenAndServe(":8080", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
 logger := slog.FromContext(r.Context()) // 提取 Logger

 logger.Info("handling request",
  "status", http.StatusOK)

 w.Write([]byte("Hello World"))
}

执行程序并访问地址: http://127.0.0.1:8080/hello

输出:

{"time":"2023-01-23T14:36:26.6303067+08:00","level":"INFO","msg":"handling request","path":"/hello","user-agent":"curl/7.83.1","status":200}

上面这种使用 Logger 的方式是不是还挺方便的,不过很遗憾的是,在最新的 slog 包里,这两个方法已经被作者移除掉了。

slog:Go官方的结构化日志包开发的怎么样了?该如何使用?

我很好奇作者为什么把这两个方法移除掉,后面翻到 slog 提案[1] 下面作者留言[2],大意是说这种使用方式有比较大的争议(主要是函数之间能否使用 context),而且如果使用者喜欢这种使用方式的话,也可以自己实现,所以把这两个方法移除了。

如果需要自己实现通过 context 储存和提取 Logger,你知道怎么实现吗?欢迎留言区交流,嘻嘻。

如何集成第三方日志包

在讲 Handler 那一节时提到过,如果我们实现了 Handler 接口,就可以将第三方 log 与 Logger 集成,那该怎么实现呢?我们就拿 logrus 日志包举例吧。

package main

import (
 "fmt"
 "github.com/sirupsen/logrus"
 "golang.org/x/exp/slog"
 "net"
 "net/http"
 "os"
)

func init() {
 // 设置logrus
 logrus.SetFormatter(&logrus.JSONFormatter{})
 logrus.SetOutput(os.Stdout)
 logrus.SetLevel(logrus.DebugLevel)
}

func main() {
 // 将 Logrus 与 Logger 集成在一块
 logger := slog.New(&LogrusHandler{
  logger: logrus.StandardLogger(),
 })

 logger.Error("something went wrong", net.ErrClosed,
  "status", http.StatusInternalServerError)
}

type LogrusHandler struct {
 logger *logrus.Logger
}

func (h *LogrusHandler) Enabled(_ slog.Level) bool {
 return true
}

func (h *LogrusHandler) Handle(rec slog.Record) error {
 fields := make(map[string]interface{}, rec.NumAttrs())

 rec.Attrs(func(a slog.Attr) {
  fields[a.Key] = a.Value.Any()
 })

 entry := h.logger.WithFields(fields)

 switch rec.Level {
 case slog.LevelDebug:
  entry.Debug(rec.Message)
 case slog.LevelInfo:
  entry.Info(rec.Message)
 case slog.LevelWarn:
  entry.Warn(rec.Message)
 case slog.LevelError:
  entry.Error(rec.Message)
 }

 fmt.Println("测试是否走了这个方法:记录日志")

 return nil
}

func (h *LogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 // 为了演示,此方法就没有实现,但不影响效果
 return h
}

func (h *LogrusHandler) WithGroup(name string) slog.Handler {
 // 为了演示,此方法就没有实现,但不影响效果
 return h
}

输出:

{"err":"use of closed network connection","level":"error","msg":"something went wrong","status":500,"time":"2023-01-23T16:07:40+08:00"}
测试是否走了这个方法:记录日志

追查代码发现,通过调用 slog 的方法记录日志时都会调用 logPC() 方法生成一条 Record,最终会交给 Handler 接口的具体实现方法 Handle(),这里就是我们自己实现的方法

func (h *LogrusHandler) Handle(rec slog.Record) error {}

从输出就可以看出,最终调用了自己实现的 Handle() 方法,走的是 logrus 包的方法 entry.Error()。

总结

这篇文章主要介绍了 slog 包的一些主要方法的使用,简单说了下里面一些函数、方法的实现,更详细的细节大家可以自行查看源码。目前中文社区关于 slog 的文章不多(可能是我没发现,欢迎补充),我发现比较好的已经在底部的参考文章里列出来了,作为补充可以深入了解 slog 包。另外感兴趣的同学可以看下关于 slog 的提案(里面会实时更新一些信息以及社区开发者的讨论)和 slog 包的设计文档,具体链接看参考文章。欢迎留言交流,一起学习成长。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

2

2026.01.29

java配置环境变量教程合集
java配置环境变量教程合集

本专题整合了java配置环境变量设置、步骤、安装jdk、避免冲突等等相关内容,阅读专题下面的文章了解更多详细操作。

2

2026.01.29

java成品学习网站推荐大全
java成品学习网站推荐大全

本专题整合了java成品网站、在线成品网站源码、源码入口等等相关内容,阅读专题下面的文章了解更多详细推荐内容。

0

2026.01.29

Java字符串处理使用教程合集
Java字符串处理使用教程合集

本专题整合了Java字符串截取、处理、使用、实战等等教程内容,阅读专题下面的文章了解详细操作教程。

0

2026.01.29

Java空对象相关教程合集
Java空对象相关教程合集

本专题整合了Java空对象相关教程,阅读专题下面的文章了解更多详细内容。

3

2026.01.29

clawdbot ai使用教程 保姆级clawdbot部署安装手册
clawdbot ai使用教程 保姆级clawdbot部署安装手册

Clawdbot是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

25

2026.01.29

clawdbot龙虾机器人官网入口 clawdbot ai官方网站地址
clawdbot龙虾机器人官网入口 clawdbot ai官方网站地址

clawdbot龙虾机器人官网入口:https://clawd.bot/,clawdbot ai是一个“有灵魂”的AI助手,可以帮用户清空收件箱、发送电子邮件、管理日历、办理航班值机等等,并且可以接入用户常用的任何聊天APP,所有的操作均可通过WhatsApp、Telegram等平台完成,用户只需通过对话,就能操控设备自动执行各类任务。

16

2026.01.29

Golang 网络安全与加密实战
Golang 网络安全与加密实战

本专题系统讲解 Golang 在网络安全与加密技术中的应用,包括对称加密与非对称加密(AES、RSA)、哈希与数字签名、JWT身份认证、SSL/TLS 安全通信、常见网络攻击防范(如SQL注入、XSS、CSRF)及其防护措施。通过实战案例,帮助学习者掌握 如何使用 Go 语言保障网络通信的安全性,保护用户数据与隐私。

8

2026.01.29

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

622

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Go 教程
Go 教程

共32课时 | 4.3万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号