zap-优雅的可选配置
Table of Contents
前瞻 #
代码中有些对象具有多种行为,而展示哪种行为方式则需要根据配置来抉择。
根据配置来实例化对象,最简单的方式是提供一个New函数来实例化对象,将配置参数作为函数入参,如:
// name和age是必传的参数,而isAdmin是可选的配置——如果是管理员,则具有更多的行为。
func NewUser(name string, age int, isAdmin bool) *User {...}
在这个例子中,User只有一个可选参数(isAdmin),在产品迭代过程中(甚至在开发过程中),会存在越来越多的功能,也会需要越来越多的可选参数,这个时候就需要去修改这个函数签名以加入更多的参数:
func NewUser(name string, age int, isAdmin bool, isTeacher bool, isStudent bool, location string) *User {...}
但是直接修改函数签名会带来一些负面效果:
- 函数签名越来越长,调用的时候需要设置很多不需要的参数
- 函数体越来越复杂,看起来很乱
- 需要修改调用方
- 。。。
所以我们一般不在New方法中存入可选参数。
我们可以借鉴下zap。
zap #
zap是uber开源的一款基础golang的日志库,以性能卓越著称。
Option接口 #
Option是一个接口,每个可选参数都实例化为一个函数,函数的返回值都实现了这个接口。
// An Option configures a Logger.
type Option interface {
apply(*Logger)
}
// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) {
f(log)
}
Option定义了apply方法,用于将配置应用于日志对象上。
optionFunc是zap中实现了Option的结构,zap中内置的可选参数所实例化的函数的返回值都是optionFunc类型。
可选参数封装为函数实例: #
这些例子有:
// Development puts the logger in development mode, which makes DPanic-level
// logs panic instead of simply logging an error.
func Development() Option {
return optionFunc(func(log *Logger) {
log.development = true
})
}
// AddCaller configures the Logger to annotate each message with the filename
// and line number of zap's caller. See also WithCaller.
func AddCaller() Option {
return WithCaller(true)
}
// WithCaller configures the Logger to annotate each message with the filename
// and line number of zap's caller, or not, depending on the value of enabled.
// This is a generalized form of AddCaller.
func WithCaller(enabled bool) Option {
return optionFunc(func(log *Logger) {
log.addCaller = enabled
})
}
// AddCallerSkip increases the number of callers skipped by caller annotation
// (as enabled by the AddCaller option). When building wrappers around the
// Logger and SugaredLogger, supplying this Option prevents zap from always
// reporting the wrapper code as the caller.
func AddCallerSkip(skip int) Option {
return optionFunc(func(log *Logger) {
log.callerSkip += skip
})
}
// AddStacktrace configures the Logger to record a stack trace for all messages at
// or above a given level.
func AddStacktrace(lvl zapcore.LevelEnabler) Option {
return optionFunc(func(log *Logger) {
log.addStack = lvl
})
}
这些函数的返回值都是optionFunc,即都实现了Option。
应用Option #
zap中通过WithOptions方法来应用可选配置:
// WithOptions clones the current Logger, applies the supplied Options, and
// returns the resulting Logger. It's safe to use concurrently.
func (log *Logger) WithOptions(opts ...Option) *Logger {
c := log.clone()
for _, opt := range opts {
opt.apply(c)
}
return c
}
example #
设置CallerSkip:
// NewStdLog returns a *log.Logger which writes to the supplied zap Logger at
// InfoLevel. To redirect the standard library's package-global logging
// functions, use RedirectStdLog instead.
func NewStdLog(l *Logger) *log.Logger {
logger := l.WithOptions(AddCallerSkip(_stdLogDefaultDepth + _loggerWriterDepth))
f := logger.Info
return log.New(&loggerWriter{f}, "" /* prefix */, 0 /* flags */)
}
替换日志核心:
func ExampleWrapCore_replace() {
// Replacing a Logger's core can alter fundamental behaviors.
// For example, it can convert a Logger to a no-op.
nop := zap.WrapCore(func(zapcore.Core) zapcore.Core {
return zapcore.NewNopCore()
})
logger := zap.NewExample()
defer logger.Sync()
logger.Info("working")
logger.WithOptions(nop).Info("no-op")
logger.Info("original logger still works")
// Output:
// {"level":"info","msg":"working"}
// {"level":"info","msg":"original logger still works"}
}
设置为开发模式:
L().With(Int("foo", 42)).Named("main").WithOptions(Development()).Info("")
设置日志级别:
logger.WithOptions(IncreaseLevel(ErrorLevel))
总结 #
zap通过Option的设计,将可选配置与日志初始化实现了“实例化的解耦”,使用者可以根据其需求而使用不同的可选项,而不同的使用者之间不会互相影响。