Description
Edit: as discussed in the comments, the proposal is not anymore about using a nil
receiver. It is still about making it cheaper to call a disabled (muted) Logger.
What
Make this nil
pointer variable
var nilLogger *log.Logger
a valid Logger, where all the calls to nilLogger.Print
, etc. are valid (do not crash), and do not produce any actual logs.
This would serve as an adequate replacement of, and almost equivalent to:
var discardLogger = log.New(io.Discard, "", 0)
Why
Performance.
This is an important concern because logging is often expensive, and it is a reasonable expectation that it becomes free (or very cheap) when logging is turned off. Currently, discardLogger
comes with a quite expensive overhead:
- all of the
discardLogger.Print
variadic arguments are evaluated, even if they are not really needed; - a non-inlined function call to
(*Logger).Print
is executed fmt.Sprint
is called, which incurs some memory allocation, and some work to use the variadic arguments in order to produce a strings
;- the mutex
discardLogger.mu
isLock
ed, then laterUnlock
ed; s
is copied to the internal bufferdiscardLogger.buf
;- a dynamic function dispatch is executed for
discardLogger.out.Write
, which resolves toio.discard.Write
.
The proposed implementation consists of checking for nil receiver inside all of the exported methods and returning immediately, thus avoiding steps 3, 4, 5, 6.
In the proposed implementation, the steps 1, 2 still happen, as the arguments are still evaluated. In theory this could be mitigated in some side-effect-free cases, with advanced escape analysis and inlining. This mitigation is out of reach of the current inlining system, and out of scope of the current proposal.
Benefits
In a sample benchmark, nilLogger.Printf
is 35x faster with an int
arg, and 320x faster with a map
arg, than discardLogger.Printf
.
The speedup would be higher with more arguments, or more complex arguments.
As a side effect, the zero value for the pointer type *log.Logger
becomes useful and safe, without initialization.
Backwards compatibility
- No changes to all the non-
nil
*log.Logger instances. log.New(io.Discard, "", 0)
still works.nilLogger.Print
would not panic anymore. A program relying on the panic behavior would break. It is however not a promise (no guarantee) from the log package to havenilLogger.Print
panic.nilLogger.Print
would not callfmt.Sprint
anymore, resulting in some customString
methods to not be called. A program relying the side effects of custom String methods would break. It is however not a promise (no guarantee) from the log package to have valid Loggers always callfmt.Sprint
.- A program may expect
l.SetOutput(w)
to always succeeds for any valid loggerl
. Such a program would break. - A program may expect
l.SetPrefix(prefix)
to force the subsequent value returned byl.Prefix()
, for any valid loggerl
. Such a program would break. - A program may expect
l.SetFlags(flag)
to force the subsequent value returned byl.Flags()
, for any valid loggerl
. Such a program would break.
Concurrency
It is my understanding that the proposed implementation does not introduce any new risks of data races.
For a nil
receiver, all of the Print
methods are thread-safe, and do not require a mutex.
Detailed behavior of each method
When the receiver l
is nil
:
(l *Logger).Fatal(v ...interface{})
(l *Logger).Fatalf(format string, v ...interface{})
(l *Logger).Fatalln(v ...interface{})
:- don't log anything. Call
os.Exit(1)
.
- don't log anything. Call
(l *Logger).Flags() int
:- return
0
.
- return
(l *Logger).Output(calldepth int, s string) error
:- do nothing. Return
nil
.
- do nothing. Return
(l *Logger).Panic(v ...interface{})
(l *Logger).Panicf(format string, v ...interface{})
(l *Logger).Panicln(v ...interface{})
:- don't log anything. Build the string
s
and callpanic(s)
.
- don't log anything. Build the string
(l *Logger).Prefix() string
:- return
""
.
- return
(l *Logger).Print(v ...interface{})
(l *Logger).Printf(format string, v ...interface{})
(l *Logger).Println(v ...interface{})
:- do nothing.
(l *Logger).SetFlags(flag int)
:- do nothing.
(l *Logger).SetOutput(w io.Writer)
:- panic. The caller would expect
w
to be subsequently used, which is not possible.
- panic. The caller would expect
(l *Logger).SetPrefix(prefix string)
:- do nothing.
(l *Logger).Writer() io.Writer
:- return
nil
.
- return