Go标准库log包默认输出到os.Stderr而非文件,需显式调用log.SetOutput或新建log.Logger实例并设置文件输出;zap因零分配、结构化、堆栈集成等优势更适生产错误日志。

Go标准库log包写入文件时为什么没有输出?
默认log.Logger实例(如log.Printf)只往os.Stderr写,不自动落盘。想存文件必须显式设置输出目标。
常见错误是直接调用log.Println后检查日志文件,发现为空——因为根本没配文件写入。
- 用
log.SetOutput重定向全局logger:f, _ := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) log.SetOutput(f) - 更安全的做法是创建独立
log.Logger实例,避免污染全局状态:logger := log.New(f, "[INFO] ", log.LstdFlags|log.Lshortfile)
- 注意:
os.OpenFile返回的*os.File需在程序退出前Close(),否则可能丢失末尾日志(尤其用defer f.Close()时要确保生命周期覆盖全程)
zap和logrus哪个更适合生产环境的错误日志?
如果追求性能与结构化,选zap;如果看重生态兼容与快速上手,logrus仍可用,但要注意它已基本停止维护(最后v1.9.3发布于2022年)。
zap在错误日志场景的优势明显:
立即学习“go语言免费学习笔记(深入)”;
- 零分配日志记录(
zap.Error(err)不触发GC),高频报错时延迟更稳 - 内置
zapcore.LevelEnablerFunc可精细控制哪些错误级别写磁盘、哪些只打屏 - 结构化字段天然支持错误堆栈:
logger.Error("db query failed", zap.String("query", q), zap.Error(err))生成JSON里会带"error": "timeout: context deadline exceeded"及完整堆栈(开启zap.AddStacktrace(zapcore.ErrorLevel)) -
logrus的WithFields是map构造,每次调用都alloc;而zap的zap.String等是无分配函数式选项
panic时如何捕获堆栈并写入错误日志?
不能只靠recover()打印字符串——那会丢掉原始panic类型、调用链深度和goroutine信息。
正确做法是结合runtime/debug.Stack()和结构化logger:
- 在顶层
defer中recover并记录:defer func() { if r := recover(); r != nil { logger.Error("panic recovered", zap.String("panic_value", fmt.Sprint(r)), zap.ByteString("stack", debug.Stack())) } }() - 注意
debug.Stack()返回的是[]byte,直接传给zap.ByteString避免转字符串再分配 - 若用
zap,建议开启zap.AddCaller(),这样每条日志都带触发位置,比堆栈里找第几行更直观 - 别在recover里再
panic(r)——这会让日志写入失败(程序立即终止),应记录后正常退出或按策略重启
多goroutine写同一日志文件会不会冲突?
不会,但前提是logger实例本身是线程安全的——log.Logger、zap.Logger、logrus.Logger都保证并发安全,内部有锁或无锁原子操作。
真正要小心的是底层io.Writer:
- 单个
*os.File本身支持并发写(系统调用层面串行化),但内容可能交错(如两个goroutine同时写"err1"和"err2",文件里出现"eerr12") - 解决方案是让logger自己做同步:用
zap.Lock()包装writer,或用lumberjack.Logger(它内部加了mutex)writeSyncer := zapcore.AddSync(&lumberjack.Logger{ Filename: "app.log", MaxSize: 100, // MB MaxBackups: 3, MaxAge: 7, // days }) - 不要自己用
sync.Mutex包一层log.Printf——这会严重拖慢日志吞吐,且掩盖了logger本已提供的并发能力










