将低级调试/跟踪日志记录语句保留在关键路径中非常有用,以便可以通过运行时配置启用它们.我们的想法是,您永远不会在生产中进行此类登录(这会削弱性能),但您可以在生产环境中将其打开(例如,生产系统脱机进行调试或设置与生产系统完全相同的测试系统.)
这种类型的日志记录有一个特殊要求:在关键路径上命中禁用的日志语句的成本必须非常低:理想情况下是一个布尔测试.
在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(...) } }
Run Code Online (Sandbox Code Playgroud)
这很接近.如果我说:
myEnabledLogger.Printf("foo %v: %v", x, y)
Run Code Online (Sandbox Code Playgroud)
它不会被格式化或禁用,如果任何记录,但它会评估参数x和y.这对于基本类型或指针是可以的,对于任意函数调用都不行 - 例如,对没有String()方法的值进行字符串化.
我看到两种方法:
推迟调用的包装类型:
type Stringify { x *Thing }
func (s Stringify) String() { return someStringFn(s.x) }
enabledLogger.Printf("foo %v", Stringify{&aThing})
Run Code Online (Sandbox Code Playgroud)
将所有内容包装在手动启用的检查中:
if enabledLog.Enabled {
enabledLog.Printf("foo %v", someStringFn(x))
}
Run Code Online (Sandbox Code Playgroud)
两者都是冗长且容易出错的,对于某人来说,忘记一步并悄悄引入令人讨厌的表现回归太容易了.
我开始喜欢Go了.请告诉我它可以解决这个问题:)
Go 中的所有参数都保证会被求值,并且该语言中没有定义的预处理器宏,因此您只能做几件事。
为了避免在日志记录参数中调用昂贵的函数调用,请使用fmt.Stringer和fmt.GoStringer接口来延迟格式化,直到函数执行为止。这样您仍然可以将普通类型传递给Printf函数。您可以使用自定义记录器自行扩展此模式,该记录器也可以检查各种接口。这就是您在示例中使用的内容Stringify,您只能通过代码审查和单元测试来真正强制执行它。
type LogFormatter interface {
LogFormat() string
}
// inside the logger
if i, ok := i.(LogFormatter); ok {
fmt.Println(i.LogFormat())
}
Run Code Online (Sandbox Code Playgroud)
您还可以通过记录器接口在运行时换出整个记录器,或者使用构建约束在构建时完全替换它,但仍然需要确保不会将昂贵的调用插入到日志记录参数中。
glog 等某些软件包使用的另一种模式是使 Logger 本身成为布尔值。这并没有完全消除冗长的内容,但它使它变得更加简洁。
type Log bool
func (l Log) Println(args ...interface{}) {
fmt.Println(args...)
}
var debug Log = false
if debug {
debug.Println("DEBUGGING")
}
Run Code Online (Sandbox Code Playgroud)
在 Go 中最接近宏预处理的是使用代码生成。这不适用于运行时可配置跟踪,但至少可以提供一个单独的调试版本,可以在需要时放置到位。它可以像gofmt -r使用 构建文件一样简单text/template,或者通过解析代码并构建 AST 来完整生成。