log包不支持日志级别,仅适合脚本或原型验证;setflags需用位运算“|”组合标志,如log.ldate | log.ltime;输出到文件需避免缓冲丢失日志,推荐lumberjack封装;性能远低于zap等结构化日志库。

log 包本身不支持日志级别,强行“模拟”分级会掩盖真实问题,也不利于后续接入结构化日志系统。真要调试,别绕弯子——该用 zap 就用,该开 DebugLevel 就开,标准库只适合脚本或原型验证。
log.SetFlags 为什么总少打一个 | ?
SetFlags 的参数是位运算组合,不是数组或字符串拼接。漏掉 | 会导致只生效最后一个标志,比如写成 log.Ldate log.Lshortfile(没竖线),实际只有 Lshortfile 生效。
- 常见错误写法:
log.SetFlags(log.Ldate log.Ltime)→ 编译报错或行为异常 - 正确写法:
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) - 开发时建议固定一套:开发用
log.Lmicroseconds | log.Lshortfile,生产可减为log.LstdFlags - 注意:
LstdFlags = Ldate | Ltime,它不含文件信息,别误以为“标准”就全包了
log 输出到文件后为啥看不到实时日志?
log.SetOutput 指向文件后,默认使用带缓冲的 *os.File,内容不会立即落盘。尤其在程序意外退出时,最后几条日志会丢失。
- 解决方法不是加
log.Printf后手动file.Sync()(太重),而是用os.OpenFile显式禁用缓冲:file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) log.SetOutput(file) - 更稳妥的做法:用
lumberjack.Logger封装,它自带Sync()调用时机控制 - 别依赖
defer file.Close()来刷日志——Close()会 flush,但程序 crash 时不会触发 defer
zap.NewDevelopment 和 zap.NewProduction 选哪个?
别看名字像“开发/生产”二选一,它们本质是两套输出逻辑:前者人类可读、带颜色、字段展开;后者机器友好、JSON 格式、压缩字段名。-
zap.NewDevelopment():适合本地调试,终端里一眼看清caller、level、msg,但日志体积大、解析成本高 -
zap.NewProduction():适合容器/K8s 环境,stdout 直接喂给日志采集器(如 fluent-bit),但本地看很反人类 - 真实项目中应通过环境变量切换:
if os.Getenv("ENV") == "dev" { logger = zap.NewDevelopment() } - 注意:
NewProduction()默认不记录caller字段,要加zap.AddCaller()才能保留文件行号
为什么 log.Printf 有时比 zap.Info 慢十倍?
慢的从来不是函数名,而是背后的 I/O 模式和格式化策略。log.Printf 是同步、无缓冲、每次调用都格式化+写入;zap.Info 默认异步、批量、预分配内存、跳过反射。
-
log.Printf在循环里打 1000 条日志,基本等于 1000 次系统调用 -
zap把日志先塞进 ring buffer,由独立 goroutine 批量刷盘 - 如果你非要用
log又想提速:自己套一层bufio.Writer,但不如直接换zap - 最容易被忽略的一点:
log.Printf("user_id: %d, name: %s", uid, name)中的%d和%s是运行时解析的,而zap.Int("user_id", uid).String("name", name)是编译期绑定字段类型,零反射开销
调试时别只盯着“有没有日志”,先确认“日志是否真的写出去了”、“写到哪儿了”、“能不能被 grep 到”。很多线上问题卡在日志根本没落地,而不是内容写得不够多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











