搜索
首页后端开发Golangslog:Go官方的结构化日志包开发的怎么样了?该如何使用?

熟悉 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.7921124+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 包的设计文档,具体链接看参考文章。欢迎留言交流,一起学习成长。

以上是slog:Go官方的结构化日志包开发的怎么样了?该如何使用?的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:Golang菜鸟。如有侵权,请联系admin@php.cn删除
使用GO编程语言构建可扩展系统使用GO编程语言构建可扩展系统Apr 25, 2025 am 12:19 AM

goisidealforbuildingscalablesystemsduetoitssimplicity,效率和建筑物内currencysupport.1)go'scleansyntaxandaxandaxandaxandMinimalisticDesignenhanceProductivityAndRedCoductivityAndRedCuceErr.2)ItSgoroutinesAndInesAndInesAndInesAndineSandChannelsEnablenableNablenableNableNablenableFifficConcurrentscorncurrentprogragrammentworking torkermenticmminging

有效地使用Init功能的最佳实践有效地使用Init功能的最佳实践Apr 25, 2025 am 12:18 AM

Initfunctionsingorunautomationbeforemain()andareusefulforsettingupenvorments和InitializingVariables.usethemforsimpletasks,避免使用辅助效果,andbecautiouswithTestingTestingTestingAndLoggingTomaintAnainCodeCodeCodeClarityAndTestesto。

INIT函数在GO软件包中的执行顺序INIT函数在GO软件包中的执行顺序Apr 25, 2025 am 12:14 AM

goinitializespackagesintheordertheordertheyimported,thenexecutesInitFunctionswithinApcageIntheirdeFinityOrder,andfilenamesdetermineTheOrderAcractacractacrosmultiplefiles.thisprocessCanbeCanbeinepessCanbeInfleccessByendercrededBydeccredByDependenciesbetenciesbetencemendencenciesbetnependendpackages,whermayleLeadtocomplexinitialitialializizesizization

在GO中定义和使用自定义接口在GO中定义和使用自定义接口Apr 25, 2025 am 12:09 AM

CustomInterfacesingoarecrucialforwritingFlexible,可维护,andTestableCode.TheyEnableDevelostOverostOcusonBehaviorBeiroveration,增强ModularityAndRobustness.byDefiningMethodSigntulSignatulSigntulSignTypaterSignTyperesthattypesmustemmustemmustemmustemplement,InterfaceSallowForCodeRepodEreusaperia

在GO中使用接口进行模拟和测试在GO中使用接口进行模拟和测试Apr 25, 2025 am 12:07 AM

使用接口进行模拟和测试的原因是:接口允许定义合同而不指定实现方式,使得测试更加隔离和易于维护。1)接口的隐式实现使创建模拟对象变得简单,这些对象在测试中可以替代真实实现。2)使用接口可以轻松地在单元测试中替换服务的真实实现,降低测试复杂性和时间。3)接口提供的灵活性使得可以为不同测试用例更改模拟行为。4)接口有助于从一开始就设计可测试的代码,提高代码的模块化和可维护性。

在GO中使用init进行包装初始化在GO中使用init进行包装初始化Apr 24, 2025 pm 06:25 PM

在Go中,init函数用于包初始化。1)init函数在包初始化时自动调用,适用于初始化全局变量、设置连接和加载配置文件。2)可以有多个init函数,按文件顺序执行。3)使用时需考虑执行顺序、测试难度和性能影响。4)建议减少副作用、使用依赖注入和延迟初始化以优化init函数的使用。

GO的选择语句:多路复用并发操作GO的选择语句:多路复用并发操作Apr 24, 2025 pm 05:21 PM

go'SselectStatementTreamLinesConcurrentProgrambyMultiplexingOperations.1)itallowSwaitingOnMultipleChannEloperations,执行thefirstreadyone.2)theDefirstreadyone.2)thedefefcasepreventlocksbysbysbysbysbysbythoplocktrograpraproxrograpraprocrecrecectefnoopeready.3)

GO中的高级并发技术:上下文和候补组GO中的高级并发技术:上下文和候补组Apr 24, 2025 pm 05:09 PM

contextancandwaitgroupsarecrucialingoformanaginggoroutineseflect.1)context contextsallowsAllowsAllowsAllowsAllowsAllingCancellationAndDeadLinesAcrossapibiboundaries,确保GoroutinesCanbestoppedGrace.2)WaitGroupsSynChronizeGoroutines,确保Allimizegoroutines,确保AllizeNizeGoROutines,确保AllimizeGoroutines

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器