真正需要的是可插拔、带上下文、支持动态 level 控制且不侵入业务的日志模块;需避免封装 log.printf 导致的结构化缺失、调用栈丢失和性能浪费,通过接口解耦输出、惰性格式化、with 方法注入上下文、原子级 level 控制及入口强制传参来实现。

Go 标准库的 log 太简陋,而直接上 zap 或 zerolog 又容易过度设计——真正需要的,是一个可插拔、带上下文、能开关 level、又不侵入业务逻辑的日志模块。
为什么别直接封装 log.Printf
很多人第一反应是写个 MyLog.Info() 包一层 log.Printf,但这会立刻踩三个坑:
• 没有结构化字段支持(比如 user_id=123 无法作为独立字段提取)
• 调用栈丢失(log.SetFlags(log.Lshortfile) 只能打文件名+行号,但无法跳过你的封装层)
• level 控制靠 if 判断,编译期无法剪枝,高频 debug 日志仍会执行参数格式化(如 fmt.Sprintf)造成性能浪费
用 io.Writer + 接口解耦输出,而非硬绑具体实现
日志最终要写到哪里?文件、stdout、网络、甚至丢弃——这些不该在 logger 接口里写死。
定义核心接口时只依赖 io.Writer:
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
}
type Field struct {
Key string
Value interface{}
}
这样你就能自由替换底层 writer:
• 测试时传 io.Discard 彻底关闭日志
• 生产用 os.OpenFile 写文件,配合 lumberjack.Logger 轮转
• 调试时接 os.Stdout 并加颜色(用 golang.org/x/term 判断是否终端)
关键点:所有日志方法内部必须检查 level 开关,且用 fmt.Sprint 替代 fmt.Sprintf 做惰性格式化(即只有真正要输出时才拼字符串)
如何安全注入请求级上下文(如 trace_id、user_id)
全局 logger 不能存 context,否则并发写入会 panic;也不能每次调用都手动传一堆字段。
推荐做法是提供 With 方法返回新实例:
func (l *logger) With(fields ...Field) Logger {
newFields := append(l.fields, fields...)
return &logger{writer: l.writer, level: l.level, fields: newFields}
}
业务代码中这样用:
reqLog := logger.With(
Field{"trace_id", traceID},
Field{"user_id", userID},
)
reqLog.Info("order created") // 自动带上 trace_id 和 user_id
注意:
• With 返回的新 logger 必须是值拷贝或深拷贝字段 slice,避免多个 goroutine 修改同一 slice
• 如果用了 zap/zerolog,它们的 With 是高效 copy-on-write;自己实现时别用指针共享原始 fields slice
• 不要在 http middleware 里把 logger 存进 context.Context,那只是把问题转移——直接在 handler 入口 reqLog := baseLogger.With(...) 更清晰
level 控制必须支持运行时动态调整,且零分配
线上出问题时,临时把 level 从 Info 切到 Debug 是刚需,但不能 reload 进程。
用 atomic.Int32 存当前 level,每个日志方法开头加判断:
if l.level.Load() > LevelDebug { return } // LevelDebug = 0, Info = 1...
同时提供 HTTP 接口或信号(如 SIGUSR1)修改该值。
容易忽略的点:
• level 比较必须用整数,别用字符串("debug" vs "info")做 runtime 判断,字符串比较慢且易错
• 所有 level 方法(Debug/Info)应内联或至少保证无 heap 分配——用 sync.Pool 管理 bytes.Buffer 或直接用 fmt.Appendf 写到栈 buffer
最麻烦的从来不是怎么打日志,而是怎么让不同服务、不同模块的日志字段对齐,以及当 trace_id 突然消失时快速定位漏了哪一层 With。别指望靠文档约束,得在代码里强制——比如所有入口函数签名都要求传 Logger,而不是从包变量取。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











