将低级调试/跟踪日志语句保留在关键路径中很有用,以便可以通过运行时配置启用它们。这个想法是您永远不会在生产环境中打开此类日志记录(这会削弱性能),但您可以在生产环境中打开它环境(例如,生产系统脱机进行调试或一个与生产系统完全一样的测试系统。)
这种类型的日志记录有一个特殊要求:在关键路径上点击禁用日志语句的成本必须非常低:理想情况下是单个 bool 测试。
在 C/C++ 中,我将使用 LOG 宏来执行此操作,该宏在检查标志之前不会评估其任何参数。只有启用后,我们才会调用一些辅助函数来格式化和传递日志消息。
那么如何在 Go 中做到这一点?
将 io.Discard 与 log.Logger 一起使用是不可取的:它每次都会完全格式化日志消息,然后如果禁用则将其丢弃。
我的第一个想法是
type EnabledLogger struct { Enabled bool; delegate *log.Logger;... }
// Implement the log.Logger interface methods as:
func (e EnabledLogger) Print(...) { if e.Enabled { e.delegate.Output(...) } }
这很接近。如果我说:
myEnabledLogger.Printf("foo %v: %v", x, y)
如果禁用,它不会格式化或记录任何内容,但它会评估参数 x 和 y。这对于基本类型或指针来说是可以的,对于任意函数调用来说是不行的——例如对没有 String() 方法的值进行字符串化。
我看到了两种解决方法:
延迟调用的包装器类型:
type Stringify { x *Thing }
func (s Stringify) String() { return someStringFn(s.x) }
enabledLogger.Printf("foo %v", Stringify{&aThing})
将整个事情包装在手动启用的检查中:
if enabledLog.Enabled {
enabledLog.Printf("foo %v", someStringFn(x))
}
两者都很冗长且容易出错,人们很容易忘记一个步骤并悄悄引入令人讨厌的性能回归。
我开始喜欢 Go。请告诉我它可以解决这个问题:)
最佳答案
Go 中的所有参数都保证会被评估,并且语言中没有定义的预处理器宏,因此您只能做几件事。
为避免在日志参数中调用昂贵的函数,请使用 fmt.Stringer
和 fmt.GoStringer
接口(interface)来延迟格式化,直到函数执行为止。这样你仍然可以将普通类型传递给 Printf
函数。您可以使用自定义记录器自己扩展此模式,该记录器也检查各种接口(interface)。这就是您在 Stringify
示例中使用的内容,您只能通过代码审查和单元测试来真正执行它。
type LogFormatter interface {
LogFormat() string
}
// inside the logger
if i, ok := i.(LogFormatter); ok {
fmt.Println(i.LogFormat())
}
您还可以在运行时通过记录器接口(interface)将整个记录器换出,或者在构建时使用 build constraints 完全替换它。 ,但仍需要确保没有将昂贵的调用插入到日志记录参数中。
glog 等一些包使用的另一种模式是让 Logger 本身成为 bool 值。这并没有完全消除冗长,但它使它更简洁一些。
type Log bool
func (l Log) Println(args ...interface{}) {
fmt.Println(args...)
}
var debug Log = false
if debug {
debug.Println("DEBUGGING")
}
在 Go 中最接近宏预处理的方法是使用代码生成。这不适用于运行时可配置跟踪,但至少可以提供一个单独的调试版本,可以在需要时放置到位。它可以像 gofmt -r
一样简单,使用 text/template
构建文件,或者通过解析代码并构建 AST 来完整生成。
关于logging - 如何在 Go 中以非常低的成本对禁用的日志语句进行跟踪日志记录,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29946675/