标准库 log 不支持日志级别,仅能全局开关输出;需封装或换用 logrus(开箱即用、支持运行时切换)或 zap(高性能、需 AtomicLevel 实现热更新)。

log 包本身不支持日志级别,硬配等于白配;真要分级输出,必须用封装、换库,或两者结合。
为什么直接调 log.SetFlags 或 log.SetPrefix 不能控制级别
因为 log 的 API 设计就是“只管输出,不管过滤”:它没有 Debug()、Warn() 这类方法,所有 Print/Printf 都无条件写入。你设的 SetPrefix("INFO: ") 只是加个字符串前缀,不是开关;SetFlags() 控制的是时间戳、文件名等元信息,和是否输出完全无关。
常见错误现象:log.Println("debug only") 在生产环境照常打印,敏感信息直接泄露。
- 标准库
log的唯一“级别”是「有无输出」——靠log.SetOutput(io.Discard)全局关闭,但这是二值开关,不是分级 - 若坚持用标准库,必须自己封装结构体或全局变量控制条件判断,比如
if level >= Debug { log.Printf(...) } - 每个
log.New()实例的 flags 和 prefix 是独立设置的,log.SetFlags()对它无效
用 logrus 快速启用分级且支持运行时切换
logrus 是最接近“开箱即用”的选择,API 直观,SetLevel() 立即生效,适合中小项目或快速验证。
安装与基础配置:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
go get github.com/sirupsen/logrus
实操建议:
- 用
logrus.SetLevel(logrus.InfoLevel)设置最低输出级别,Debug()就会被静默丢弃 - 从环境变量读取级别更灵活:
level, _ := logrus.ParseLevel(os.Getenv("LOG_LEVEL")),然后logrus.SetLevel(level) - 开发时加
logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}),生产可换JSONFormatter - 注意:默认输出到
os.Stderr,若想合并到 stdout,需显式调用logrus.SetOutput(os.Stdout)
用 zap 实现高性能分级并支持热更新
zap 的级别控制不在 Logger 实例上,而在底层 core;直接调 logger.WithOptions(zap.IncreaseLevel()) 完全无效——那只是初始化时的临时修饰。
正确做法是绑定一个可变的 AtomicLevel:
- 定义全局变量:
var atomicLevel = zap.NewAtomicLevel() - 构造
core时传入:core := zapcore.NewCore(enc, sink, atomicLevel) - 创建
logger := zap.New(core) - 运行时切换:
atomicLevel.SetLevel(zap.DebugLevel)或atomicLevel.SetLevel(zap.WarnLevel) - 如果用了自定义
core包装器(如加采样),务必在Check()方法里调用atomicLevel.Level(),否则热更新失效
别忽略输出目标和结构化字段的耦合影响
日志级别只是过滤逻辑的一环;真正落地时,输出位置(os.Stdout vs 文件 vs syslog)、编码格式(文本 vs JSON)、上下文字段(req_id、user_id)都会影响排查效率。
容易被忽略的点:
- 多个
WriteSyncer(如同时写文件+控制台)必须用zapcore.NewMultiCore()组合,不能简单拼[]zapcore.Core -
logrus.WithField("req_id", "abc123").Info("login")是线程安全的,但反复调用会分配新对象;高频场景建议复用logrus.Entry实例 - 文件日志没轮转机制?标准库和
logrus都不带;zap需配合lumberjack或自己实现WriteSyncer - 所有库在进程退出前都必须调
logger.Sync()(zap)或logrus.Flush()(logrus),否则缓冲区日志丢失

















