Как вести журнал трассировки в Go с очень низкой стоимостью для отключенных операторов журнала

Полезно оставлять низкоуровневые заявления об отладке/трассировке в критических путях, чтобы их можно было включить с помощью конфигурации времени выполнения. Идея заключается в том, что вы никогда не включаете такой журнал в производство (это может привести к нарушению производительности), но вы можете включить его в производственной среде (например, производственная система отключена для отладки или тестовой системы, которая настроена точно так же, как и в производственной системе).

В этом типе ведения журнала есть специальное требование: стоимость нажатия на отключенный оператор журнала на критическом пути должна быть очень низкой: в идеале это один логический тест.

В 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. Скажите, пожалуйста, это может решить эту проблему:)

Ответ 1

Все аргументы в 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())
}

Вы также можете поменять весь журнал на время выполнения через интерфейс регистратора или полностью заменить его на время сборки с помощью ограничений сборки, но по-прежнему требует обеспечения того, чтобы дорогие вызовы не вставлялись в аргументы ведения журнала.

Другой шаблон, используемый некоторыми пакетами, такими как glog, - сделать сам Logger bool. Это не полностью устраняет многословие, но делает его немного более кратким.

type Log bool
func (l Log) Println(args ...interface{}) {
    fmt.Println(args...)
}

var debug Log = false

if debug {
    debug.Println("DEBUGGING")
}

Ближе всего вы можете перейти к предварительной обработке макросов, чтобы использовать генерацию кода. Это не будет работать для настраиваемой трассировки времени исполнения, но может по крайней мере обеспечить отдельную сборку отладки, которая может быть вставлена ​​на место, когда это необходимо. Это может быть так же просто, как gofmt -r, создание файла с помощью text/template или полного поколения путем анализа кода и построения AST.

Ответ 2

Прежде всего, я должен сказать, что лично я пошёл бы с самым простым решением и просто использовал if. Но если вы действительно хотите сделать это надежно, вы можете использовать анонимные функции:

type verbose bool

func (v verbose) Run(f func()) {
    if v {
        f()
    }
}

Используйте его следующим образом:

v := verbose(false)
v.Run(func() {
    log.Println("No Print")
})

Пример площадки с дорогими вызовами: http://play.golang.org/p/9pm2HoQQ8A.