I personally favor treating it as any other normal dependency and pass it where it is need it
package foo
type Bar struct {
Logger logger.Logger
}
func (b *Bar) Something() {
b.Logger.Debug("starting something")
}
func DoSomething(logger logger.Logger) {
b := Bar{Logger: logger}
b.Something()
}
Anything that involves an init function is basically a global variable
I also tend to use an abstraction over any logger that I use, and give the zero value a no-op behavior (doesn't log anything), especially helpful while testing, the downside it's a bit slower since all method are not a pointer receiver and require a copy and that I have to define the same methods (in fact I abstract the sugared version so I don't import zap on my packages)
package logger
import "go.uber.org/zap"
type Logger struct {
zap *zap.Logger
}
func Must(logger *Logger, err error) *Logger {
if err != nil {
panic(err)
}
return logger
}
func NewLogger(logFile string) (*Logger, error) {
zap.NewProductionConfig()
config := zap.NewProductionConfig()
config.OutputPaths = []string{"stdout", "./logs/" + logFile}
logger, err := config.Build(zap.AddCaller())
if err != nil {
return nil, err
}
return &Logger{zap: logger}, err
}
func (l Logger) Debug(msg string, fields ...zap.Field) {
l.writer().Debug(msg, fields...)
}
func (l Logger) Info(msg string, fields ...zap.Field) {
l.writer().Info(msg, fields...)
}
// define all the methdos
var noOpLogger = zap.NewNop()
func (l Logger) writer() *zap.Logger {
if l.zap == nil {
return noOpLogger
}
return l.zap
}
The zero value no-op logger is safe for concurrent use and the logger doesn't get in the middle anymore during testing
var b Bar
b.Something() // no panics