当前位置:首页 > 文章列表 > Golang > Go教程 > Go io.MultiWriter 如何同时写文件和摘要哈希

Go io.MultiWriter 如何同时写文件和摘要哈希

来源:17golang原创 2026-09-12 09:44:20 0浏览 收藏

需要把下载流保存到文件,同时计算 SHA-256 摘要时,Go 不必先把全部内容读进内存。把文件和哈希对象交给 io.MultiWriter,再用 io.Copy 消费同一个 io.Reader 即可。不过它只负责“按顺序转发写入”,不负责回滚:某个 Writer 失败后,后面的 Writer 不会再收到当前这块数据。

要点速览
  • io.MultiWriter(file, hash) 会先写文件,再写摘要;顺序就是失败边界。
  • 返回的 nerr 必须一起判断,短写会被报告为 io.ErrShortWrite
  • 要避免半截正式文件,先写临时文件,复制成功并关闭后再 Rename

io.MultiWriter 为什么能同时写文件和摘要

io.MultiWriter 接收多个 io.Writer,返回一个新的 Writer。每次调用它的 Write,内部都会把同一个字节切片依次交给列表中的 Writer。它不是并行广播器,也不是事务容器;传入顺序会影响失败后的状态。

Go io.MultiWriter 将 Reader 的同一字节流连接到文件 Writer 与 SHA-256 Hash 的静态关系图
图1:看清 Reader、io.MultiWriter、文件 Writer 和 SHA-256 Hash 的静态连接关系。

例如文件 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 的撤销接口。

Go 文件写入、SHA-256 摘要、关闭检查和 Rename 提交边界的静态结构图
图2:把临时文件写入、摘要计算、关闭检查与正式文件提交分成清晰边界。

生产写法:临时文件成功后再替换正式文件

如果正式路径不能出现半截内容,把 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 接收的是同一份字节流。
  • 同时判断 writtenerr,并把短写当成失败。
  • 失败后删除临时文件;不要把失败时的中间摘要当成最终摘要。
  • 提交前检查 Close,提交后再持久化摘要和文件元数据。
  • 并发写同一目标时,在外层增加锁或版本控制; MultiWriter 本身不提供业务级并发协调。

常见问题

io.MultiWriter 会并行写多个目标吗?

不会。标准库按传入顺序逐个调用 Writer;需要并行时必须自行设计并发、背压和错误汇总。

文件成功但哈希失败会怎样?

文件可能已经包含当前数据,而摘要没有同步更新。遇到这种自定义哈希 Writer 错误,应把本次结果视为失败并清理或隔离文件。

能不能用 bytes.Buffer 先缓存来避免半文件?

小数据可以,但大文件会增加内存占用。更稳妥的通用方案是临时文件写入完成后再重命名。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Python 3.14 多解释器如何分配独立任务Python 3.14 多解释器如何分配独立任务
上一篇
Python 3.14 多解释器如何分配独立任务
Linux cgroup v2 cpu.max 如何判断 CPU 配额
下一篇
Linux cgroup v2 cpu.max 如何判断 CPU 配额
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    97次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    28次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    252次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    180次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    111次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码