Go io.MultiWriter 如何同时写文件和摘要哈希
需要把下载流保存到文件,同时计算 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 先缓存来避免半文件?
小数据可以,但大文件会增加内存占用。更稳妥的通用方案是临时文件写入完成后再重命名。
Python 3.14 多解释器如何分配独立任务
- 上一篇
- Python 3.14 多解释器如何分配独立任务
- 下一篇
- Linux cgroup v2 cpu.max 如何判断 CPU 配额
-
- Golang · Go教程 | 9分钟前 |
- Go fs.WalkDir 如何在遇到权限错误时保留其他目录
- 253浏览 收藏
-
- Golang · Go教程 | 46分钟前 | golang · io.Reader · EOF · 流式读取 · 字节限制 · Go io.Reader io.LimitReader 读取上限 LimitedReader
- Go io.Reader 如何限制单次读取的最大字节数
- 299浏览 收藏
-
- Golang · Go教程 | 1小时前 | 字符串 · 标准库 · Go教程 · 并发边界 · 内存语义 · Go string 字符串拼接 strings.Builder strings.Clone
- Go strings.Builder 写入后如何避免返回字符串被意外修改
- 236浏览 收藏
-
- Golang · Go教程 | 1小时前 | go · 二进制 · bytes.Buffer · bytes.Buffer Go二进制拼接 Go缓冲区复用
- Go bytes.Buffer 如何复用来拼接多段二进制数据
- 396浏览 收藏
-
- Golang · Go教程 | 17小时前 |
- Go time.Timer Reset 前为什么要先确认旧定时器状态
- 346浏览 收藏
-
- Golang · Go教程 | 18小时前 |
- Go time.ParseInLocation 夏令时重复时间点如何记录来源时区
- 320浏览 收藏
-
- Golang · Go教程 | 18小时前 | go · 时区 · time.Parse · time.ParseInLocation ·
- Go time.ParseInLocation Parse 和 ParseInLocation 读取同一文本为何不同
- 156浏览 收藏
-
- Golang · Go教程 | 18小时前 | 时区 · Go教程 · 时间解析 · time.ParseInLocation · 实战排错 · Go 时间处理 time.ParseInLocation 时区解析
- Go time.ParseInLocation 解析无时区字符串怎么避免时区漂移
- 372浏览 收藏
-
- Golang · Go教程 | 18小时前 |
- Go compress/gzip Writer.Flush 什么时候会增加网络延迟
- 472浏览 收藏
-
- Golang · Go教程 | 18小时前 | go · gzip · 压缩文件 · Go compress/gzip Header.Name
- Go compress/gzip Header.Name 如何影响生成文件元信息
- 332浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 97次使用
-
- OpenCompass
- OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
- 28次使用
-
- SuperCLUE
- SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
- 252次使用
-
- C-Eval
- 深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
- 180次使用
-
- AI Prompt Library
- 探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
- 111次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

