将低级调试/跟踪日志记录语句保留在关键路径中非常有用,以便可以通过运行时配置启用它们。这个想法是你永远不会在生产中打开这样的登录(它会削弱性能),但你可以在生产环境中打开它(例如生产系统离线进行调试或一个与生产系统完全相同的测试系统。)
这种类型的日志记录有一个特殊要求:在关键路径上命中禁用日志语句的成本必须非常低:理想情况下是一个布尔测试。< / p>
在C / C ++中,我会使用一个LOG宏来执行此操作,该宏在检查标志之前不会评估任何参数。只有在启用时我们才会调用一些辅助函数来格式化&amp;发送日志消息。
那么如何在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了。请告诉我它可以解决这个问题:)
答案 0 :(得分:2)
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())
}
您还可以通过记录器界面在运行时将整个记录器换出,或者在构建时使用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来完整生成。