go log包不支持日志级别,需用多个*log.logger实例模拟,如error/info各用独立实例并统一输出目标;级别开关靠变量控制,不可共用实例加前缀。

Go 标准库 log 包本身不支持日志级别,所谓“封装实现级别”,本质是绕过缺失能力的工程权衡——不是加个函数就能有 Debug/Info/Warn,而是得明确你想要什么:轻量可控?结构化输出?运行时可调?还是为将来换 zap 预留接口?选错方向,后期改起来更痛。
用多个 *log.Logger 实例模拟级别最直接
适合脚本、CLI 工具或小服务,零依赖,启动快,逻辑透明。核心是为每个级别创建独立 logger,并统一控制输出目标和标志位。
-
log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lshortfile)和log.New(os.Stdout, "[INFO] ", log.LstdFlags|log.Lshortfile)必须分开实例,不能共用一个*log.Logger后靠前缀区分——否则无法单独关闭某一级别 - 级别开关靠变量控制:定义
var logLevel = INFO,每次调用前判断if logLevel 再写入,避免无意义格式化开销 - 别在
init()里用log.SetOutput全局重定向——它会影响所有 logger 实例,破坏分级隔离 - 如果要支持日志文件轮转,不要自己拼接日期后缀并
os.Rename;优先用lumberjack.Logger这类成熟轮子,否则容易在并发写入时丢日志或 panic
zap 不是装上就完事,NewDevelopment() 和 NewProduction() 行为差异极大
很多团队上线后发现日志变少、没颜色、字段名缩写、JSON 解析失败——大概率是本地用 zap.NewDevelopment() 调试,上线却误用 zap.NewProduction(),或反过来。
-
zap.NewDevelopment()默认启用DebugLevel、彩色输出、完整调用栈、CallerSkip为 2,适合 dev 环境肉眼排查 -
zap.NewProduction()默认只输出InfoLevel及以上、禁用颜色、字段名压缩(如"level"→"l")、强制同步刷盘,且EncodeLevel默认用zapcore.CapitalLevelEncoder,输出的是"LEVEL":"INFO",不是小写"level":"info" - 想临时提级(比如某个 HTTP handler 打 Debug),别动全局 logger,用
logger.WithOptions(zap.IncreaseLevel(zap.DebugLevel))派生新实例 - 自定义
zapcore.NewCore时,必须显式传zapcore.AddSync(os.Stdout),否则日志根本不会输出到终端
logrus.SetLevel() 是 goroutine 不安全的,别在运行时乱调
遗留项目还在用 logrus 的,务必注意:它的 SetLevel 直接写全局变量 logrus.level,没有锁保护。
- 多个 goroutine 同时执行
logrus.SetLevel(logrus.DebugLevel)和logrus.SetLevel(logrus.WarnLevel),最终值可能错乱,导致 Debug 日志意外涌出 - 正确做法是启动时一次设好,例如:
logrus.SetLevel(logrus.LevelFromString(os.Getenv("LOG_LEVEL"))) - 真需要运行时切换(如 admin 接口动态调级),必须自己加
sync.RWMutex包一层,且调用时用logrus.StandardLogger().SetLevel(...)替代裸调logrus.SetLevel(...) -
logrus已停止维护,新项目别再引入;老项目迁移成本不高,zap的Logger接口兼容性足够好,替换时业务代码几乎不用改
封装层不是加个 wrapper 就叫“解耦”,关键在接口契约
很多人封装 logger.Info() 只是简单转发给 logrus 或 zap,结果换库时发现业务代码里到处是 logger.WithField("user_id", id) 这种结构化调用——这已经和具体库强绑定了。
- 真正可切换的封装,只暴露最简接口:
Info(string)、Infof(string, ...interface{})、Error(string)等,不暴露WithField、With、Named等库特有方法 - 结构化日志需求应通过参数传递,比如
Info("user login", "user_id", 123, "ip", "10.0.0.1"),由封装层内部转成zap的Any或logrus的Fields - 初始化入口必须收口,例如
logger.Init(level, output),禁止业务代码直接 import 第三方日志库 - 最容易被忽略的一点:封装层的日志级别变量(如
minLevel)必须是包级变量 + 初始化时锁定,而不是靠每次调用都读环境变量——后者在高并发下会成为性能瓶颈
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











