log.setoutput 无法直接写多个文件,因标准库 log.logger 仅接受单个 io.writer;需自定义 multiwriter 结构体同步分发日志到多个目标,并处理错误、并发与资源释放。

Log.SetOutput 无法同时写多个文件?
go 标准库的 log.Logger 只接受单个 io.Writer,直接传入多个文件或通道会编译报错。这不是设计缺陷,而是刻意保持简单——分发逻辑本就不该由日志器自己承担。
常见错误现象:cannot use multiWriter (type io.Writer) as type io.Writer in argument to log.SetOutput(其实是类型没错,但你写的 multiWriter 没正确实现 Write 方法)。
- 正确做法是自己实现一个组合
io.Writer,把一次Write转发给多个下游 - 注意:必须同步写入所有目标,否则日志错乱;若某一个 Writer 写失败(如磁盘满),是否阻塞、忽略、还是 panic,得你自己决定
- 别用
io.MultiWriter直接包文件句柄——它不处理写入错误,也不支持关闭控制,线上容易丢日志
如何安全地实现多路写入器(MultiWriter)
核心是封装一个结构体,持有多个 io.Writer 切片,并在 Write 方法里遍历调用。关键在于错误处理策略和并发安全。
使用场景:同时输出到文件 + 控制台 + 网络 hook(比如发 Slack),且要求不丢失、可观察失败。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Mutex保护写入过程,避免 goroutine 并发写导致日志行撕裂 - 每个
Write调用都单独判断错误,建议记录第一个失败的错误,但继续尝试其余目标(避免单点故障中断全部输出) - 不要在
Write里做耗时操作(如 http 请求),否则会拖慢主流程;真要发网络,应异步投递并带背压 - 示例片段:
type MultiWriter struct { writers []io.Writer mu sync.Mutex } func (m *MultiWriter) Write(p []byte) (n int, err Error) { m.mu.Lock() defer m.mu.Unlock() for _, w := range m.writers { if n, err := w.Write(p); err != nil { return n, err // 或者只 warn,不 return } } return len(p), nil }
为什么不用 logrus/zap 而坚持用标准 log
标准 log 包轻量、无依赖、启动快,适合嵌入式、CLI 工具或对二进制体积敏感的场景。但它的多路分发必须手动搭,不像 logrus 的 Hook 或 zap 的 Core 那样开箱即用。
性能影响:标准包无缓冲、无异步、无字段结构化,纯文本追加写;如果日志量大且目标多(尤其含网络),会明显卡主 goroutine。
- 兼容性没问题:所有 Go 版本都自带
log,无需额外依赖管理 - 但缺失功能也真实存在:没有日志级别开关(
log.printf全部输出)、不能自动加时间戳(得靠log.LstdFlags)、不支持 json 格式 - 如果你只需要 INFO+ERROR 分别写两个文件,标准包 + 自定义
MultiWriter更干净;如果还要采样、上报、上下文注入,就该换 zap
文件 Writer 的生命周期和 panic 风险
用 os.OpenFile 创建的 *os.File 是资源句柄,必须显式 Close。但 log.Logger 不管这个,也不会帮你关——你把它塞进 MultiWriter 后,就彻底失去控制权了。
容易踩的坑:log.SetOutput 后程序退出前没关文件,导致下一次启动时“permission denied”或“too many open files”。
- 解决方案一:用
lumberjack.Logger替代裸*os.File,它自动轮转、自动关旧文件 - 解决方案二:不把
*os.File直接扔进MultiWriter,而是包装一层带 close 方法的结构体,在程序退出时统一调用 - 千万别 defer Close() 在初始化函数里——那会立刻关掉,后续日志全写失败
- 错误信息示例:
open /var/log/app.log: too many open files,说明你反复新建文件没关,而不是MultiWriter本身的问题
事情说清了就结束。最麻烦的从来不是怎么分发,而是每个目的地的可靠性边界在哪——文件会不会满、网络会不会超时、要不要重试、失败了告不告诉调用方。这些得按实际服务等级去抠,没法靠一个接口搞定。