io.multiwriter按顺序同步写入各目标,任一write失败即停止且不回滚,返回首个错误及最小写入字节数,不并发、不重试、不保证原子性。

io.MultiWriter 的核心行为:写入是同步且不可中断的
io.MultiWriter 不是“并发写入”,而是按顺序把同一份数据依次写进所有传入的 io.Writer。它内部用一个循环调用每个 writer 的 Write 方法,只要其中任意一个返回错误(比如 os.Stderr 被关闭、bytes.Buffer 写满),整个写入就立即失败并返回该错误。
这意味着你不能靠它来“容错”——比如一个 writer 失败了还想继续写别的。它更像一个写入分发器,而非重试或降级机制。
- 所有 writer 必须支持重复调用
Write,且行为符合预期(例如log.Writer()可以,但某些自定义 writer 若内部状态不一致可能出问题) - 如果某个 writer 的
Write阻塞(如网络 writer 未设超时),整个io.MultiWriter.Write就会卡住 - 返回的写入字节数是所有 writer 中写入最少的那个(取最小值),不是总和
典型使用场景:日志复制、调试输出、审计记录
最常见的需求是把日志同时写到文件 + 控制台 + 网络端点。这时你要确保每个目标都准备好接收数据,且能独立处理错误。
示例:把一条消息同时写入 os.Stdout、bytes.Buffer 和自定义的带时间戳 writer:
var buf bytes.Buffer
mw := io.MultiWriter(os.Stdout, &buf, ×tampWriter{})
n, err := mw.Write([]byte("hello world\n"))
// n == 12,err 取决于三个 writer 中最先失败的那个
- 注意:不要把
os.Stdout和os.Stderr同时传入,除非你明确接受它们共享缓冲行为(比如行缓冲冲突) - 如果目标之一是
log.Logger的输出(l.Writer()),确认该 logger 没有额外加锁或格式化逻辑干扰原始字节流 - 避免传入 nil pointer 的 writer,会导致 panic;建议在构造前做非空检查
与 goroutine 并发写入的本质区别
有人误以为 io.MultiWriter 能提升吞吐量,其实它不会并发执行。真正需要并发写入多个目标(比如不互相阻塞),得自己用 goroutine 包装:
func concurrentWrite(writers []io.Writer, p []byte) error {
var wg sync.WaitGroup
var mu sync.Mutex
var firstErr error
for _, w := range writers {
wg.Add(1)
go func(w io.Writer) {
defer wg.Done()
_, err := w.Write(p)
if err != nil {
mu.Lock()
if firstErr == nil {
firstErr = err
}
mu.Unlock()
}
}(w)
}
wg.Wait()
return firstErr
}
- 这种模式下,一个 writer 失败不影响其他 writer 继续写
- 但你也失去了
io.MultiWriter提供的原子性语义(即“全写成功”或“立刻失败”) - 务必控制 goroutine 数量,尤其 writer 是网络连接时,避免 fd 耗尽
容易忽略的细节:Write 返回值和错误传播
io.MultiWriter.Write 的返回值 (int, error) 容易被误解。它返回的是“所有 writer 中写入字节数的最小值”,而不是总和。这在调试时尤其关键——比如你往一个 bytes.Buffer(容量只剩 2 字节)和一个 os.File(完全可用)里写 10 字节,结果是 n == 2, err == nil,看起来像只写了 2 字节成功,实际 os.File 可能已写满 10 字节。
- 若需精确知道每个 writer 写了多少,必须单独调用它们的
Write方法 - 错误只取第一个非 nil 错误,后续 writer 即使也失败也不会暴露出来
- 没有内置重试、超时、上下文取消支持,要加这些得在外层封装
真正复杂的地方不在怎么用,而在于你是否清楚它不做什么:它不并发、不重试、不聚合错误、不区分目标类型。用之前先想清楚,你要的到底是“分发”还是“并行”。











