go标准库log包无内置日志级别机制,无法动态过滤输出;需封装带level字段的logger结构体,用整数比较实现高效级别控制,并避免未启用日志的参数计算开销。

为什么直接用 log 包没法做层级输出
Go 标准库的 log 包只提供基础输出能力,没有内置「日志级别」(如 debug/info/warn/error)概念,更不支持动态控制某一层级是否打印。你调用 log.Print 就一定输出,无法按需过滤或着色。如果硬加 if 判断,会污染业务逻辑,也难以统一管理。
用 log.SetFlags 和自定义 Writer 不够用
有人试图靠 log.SetFlags(log.Lshortfile) 或包装 io.Writer 来加前缀,但这只能改格式,不能解决「哪些日志该出现、哪些该静默」这个核心问题。真正需要的是:一个带状态的日志实例,能接收 Debugf、Infof 等方法调用,并根据当前设置决定是否写入。
- 必须封装一个结构体,持有
level字段(比如int或枚举) - 每个日志方法(
Debugf、Warnf)要先比对自身级别是否 ≤ 当前启用级别 - 避免每次调用都反射或字符串比较——用整数比较最快,例如
DEBUG=0,INFO=1,ERROR=3 - 别把
fmt.Sprintf放在判断前,否则即使日志被过滤,参数计算和内存分配仍会发生
推荐实现:轻量结构体 + 可变输出目标
下面是一个生产可用的最小骨架,支持设置级别、写入任意 io.Writer(包括文件、os.Stderr、甚至网络连接):
type Logger struct {
level int
out io.Writer
}
const (
DEBUG = iota
INFO
WARN
ERROR
)
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.level
<p>注意:<code>fmt.Fprintf</code> 比 <code>fmt.Printf</code> 更可控,因为你明确指定写入目标;<code>os.Stderr</code> 是默认错误输出流,适合调试;若要写文件,传 <code>os.File</code> 即可。</p>
<h3>容易被忽略的细节:日志前缀竞争与并发安全</h3>
<p>上面代码没加锁,是因为 Go 的 <code>fmt.Fprintf</code> 本身不是并发安全的——多个 goroutine 同时写同一个 <code>io.Writer</code>(尤其是终端或文件)会导致输出错乱,比如两行日志挤在同一行。这不是层级逻辑的问题,而是 I/O 层的现实约束。</p>
- 简单方案:用
sync.Mutex包裹每次Fprintf调用(适合吞吐不高场景) - 进阶方案:用
log.SetOutput配合标准log包的线程安全 Writer(但会失去自定义级别控制) - 最稳妥:用现成库如
zap或zerolog,它们内部已处理缓冲、原子写、采样等细节
自己造轮子时,别为了“简洁”跳过并发保护——线上服务一旦日志混杂,排查成本远高于加几行锁。











