Go io.MultiWriter 如何同时写文件和摘要哈希
来源:17golang原创
时间:2026-09-12 09:44:20 215浏览 收藏
需要把下载流保存到文件,同时计算 SHA-256 摘要时,Go 不必先把全部内容读进内存。把文件和哈希对象交给 io.MultiWriter,再用 io.Copy 消费同一个 io.Reader 即可。不过它只负责“按顺序转发写入”,不负责回滚:某个 Writer 失败后,后面的 Writer 不会再收到当前这块数据。
io.MultiWriter(file, hash)会先写文件,再写摘要;顺序就是失败边界。- 返回的
n与err必须一起判断,短写会被报告为io.ErrShortWrite。 - 要避免半截正式文件,先写临时文件,复制成功并关闭后再
Rename。
io.MultiWriter 为什么能同时写文件和摘要
io.MultiWriter 接收多个 io.Writer,返回一个新的 Writer。每次调用它的 Write,内部都会把同一个字节切片依次交给列表中的 Writer。它不是并行广播器,也不是事务容器;传入顺序会影响失败后的状态。

例如文件 Writer 放在前面时,文件写入成功后才会更新哈希;文件写失败,当前这块数据不会到达哈希。若哈希放在前面,则文件失败时摘要可能已经前进。两种顺序都不能撤销已经成功的写入,所以生产代码还要有临时文件或失败清理策略。
最小实现:一份输入写入文件并计算 SHA-256
下面的函数适合已经接受“失败时删除目标文件或由调用方处理半成品”的场景。文件放在第一位,意味着文件成功接收当前数据后,摘要才接收同一块数据。
package main
import (
"crypto/sha256"
"fmt"
"io"
"os"
)
// saveAndDigest 将同一个 Reader 写入文件,同时返回完整数据的 SHA-256。
func saveAndDigest(src io.Reader, path string) (string, error) {
// O_CREATE|O_TRUNC 确保旧文件不会和本次内容拼接;权限按业务需要调整。
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return "", fmt.Errorf("打开目标文件: %w", err)
}
defer file.Close() // 关闭错误在临时文件版本中单独检查。
digest := sha256.New()
writer := io.MultiWriter(file, digest)
// io.Copy 返回已写入字节数和遇到的第一个错误,不能只检查字节数。
written, err := io.Copy(writer, src)
if err != nil {
return "", fmt.Errorf("复制 %d 字节后失败: %w", written, err)
}
return fmt.Sprintf("%x", digest.Sum(nil)), nil
}
这里的“同时”是同一次复制过程中的双路写入,不代表两个目标并行执行。sha256.Hash.Write 通常不会返回错误,因此文件 Writer 的错误最常见;但自定义 Writer 仍可能短写或返回错误,不能假设所有目标都可靠。
失败时怎么判断文件和摘要是否还能配对
先看返回值,再看输出状态。io.Writer 约定当返回的 n 小于输入长度时必须同时返回非空错误;io.MultiWriter 对没有报错却短写的 Writer 会返回 io.ErrShortWrite。一旦任一 Writer 报错,当前写入停止,后面的目标不会补写。
| 结果 | 应如何处理 |
|---|---|
err == nil | 复制完整;此时再读取 digest.Sum(nil) |
err == io.ErrShortWrite | 视为输出不完整,删除或隔离半成品 |
| 文件写入错误 | 摘要可能只覆盖此前已成功转发的数据,不能当作完整文件摘要 |
| 关闭文件失败 | 仍应判定提交失败,尤其是网络盘或带缓存的文件系统 |
所以不要在 io.Copy 返回错误时继续把摘要写入数据库,也不要只依据文件已经存在就认为保存成功。MultiWriter 没有跨 Writer 的撤销接口。

生产写法:临时文件成功后再替换正式文件
如果正式路径不能出现半截内容,把 io.MultiWriter 的文件目标改成同目录临时文件。复制成功后先关闭,再调用 os.Rename;任一步失败都清理临时文件。摘要只在提交前返回,调用方就不会拿到一份未落盘完成的校验值。
func saveAtomically(src io.Reader, finalPath string) (string, error) {
// 临时文件放在目标目录,便于 Rename 保持在同一文件系统内完成。
dir := filepath.Dir(finalPath)
tmp, err := os.CreateTemp(dir, ".payload-*")
if err != nil {
return "", fmt.Errorf("创建临时文件: %w", err)
}
tmpName := tmp.Name()
committed := false
defer func() {
if !committed {
_ = os.Remove(tmpName) // 失败路径只清理本次临时文件。
}
}()
digest := sha256.New()
written, err := io.Copy(io.MultiWriter(tmp, digest), src)
if err != nil {
_ = tmp.Close()
return "", fmt.Errorf("写入 %d 字节后失败: %w", written, err)
}
// Close 失败也不能提交,避免把缓存或元数据错误隐藏起来。
if err := tmp.Close(); err != nil {
return "", fmt.Errorf("关闭临时文件: %w", err)
}
if err := os.Rename(tmpName, finalPath); err != nil {
return "", fmt.Errorf("提交文件: %w", err)
}
committed = true
return fmt.Sprintf("%x", digest.Sum(nil)), nil
}
示例还需要在 import 中加入 path/filepath。同一目录是为了降低跨文件系统重命名失败的概率;若业务还要求断电后的持久性,可在提交策略中加入 Sync,但这属于存储可靠性设计,不是 MultiWriter 自动提供的能力。
上线前的检查清单
- 确认输入 Reader 只消费一次,文件 Writer 与哈希 Writer 接收的是同一份字节流。
- 同时判断
written、err,并把短写当成失败。 - 失败后删除临时文件;不要把失败时的中间摘要当成最终摘要。
- 提交前检查
Close,提交后再持久化摘要和文件元数据。 - 并发写同一目标时,在外层增加锁或版本控制;
MultiWriter本身不提供业务级并发协调。
常见问题
io.MultiWriter 会并行写多个目标吗?
不会。标准库按传入顺序逐个调用 Writer;需要并行时必须自行设计并发、背压和错误汇总。
文件成功但哈希失败会怎样?
文件可能已经包含当前数据,而摘要没有同步更新。遇到这种自定义哈希 Writer 错误,应把本次结果视为失败并清理或隔离文件。
能不能用 bytes.Buffer 先缓存来避免半文件?
小数据可以,但大文件会增加内存占用。更稳妥的通用方案是临时文件写入完成后再重命名。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
117 收藏
-
195 收藏
-
253 收藏
-
378 收藏
-
Golang · Go教程 | 1小时前 | golang · io.Reader · EOF · 流式读取 · 字节限制 · Go io.Reader io.LimitReader 读取上限 LimitedReader299 收藏
-
Golang · Go教程 | 2小时前 | 字符串 · 标准库 · Go教程 · 并发边界 · 内存语义 · Go string 字符串拼接 strings.Builder strings.Clone236 收藏
-
396 收藏
-
346 收藏
-
281 收藏
-
320 收藏
-
156 收藏
-
Golang · Go教程 | 19小时前 | 时区 · Go教程 · 时间解析 · time.ParseInLocation · 实战排错 · Go 时间处理 time.ParseInLocation 时区解析372 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习