当前位置:首页 > 文章列表 > Golang > Go教程 > Go 怎么把多个文件打包成 ZIP 并提供下载

Go 怎么把多个文件打包成 ZIP 并提供下载

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

接口原本只返回一个文件,后来需求变成“把报告、数据和说明一起下载”。最省事的做法不是先把所有文件读进 bytes.Buffer,而是使用标准库 archive/zip,把文件逐个写入 ZIP,再以附件响应返回。需要特别记住两点:ZIP 条目名要自己规划,zip.Writer.Close() 必须成功调用,否则中央目录没有写完,客户端拿到的往往只是损坏压缩包。

要点速览
  • zip.NewWriter 创建归档,Create 创建每个内部文件。
  • 使用相对且唯一的条目名,不能把用户传入路径原样写入 ZIP。
  • 先完成并关闭 ZIP,再设置下载响应;中途失败要清理临时文件。

先把文件列表变成稳定的 ZIP 条目

先把“物理路径”和“压缩包内名称”分开。物理路径用于服务端打开文件,条目名只描述下载者解压后看到的目录结构。这样既能保留报告目录,也不会把服务器绝对路径暴露给客户端。

Go archive/zip 将服务端文件列表映射为唯一相对 ZIP 条目的静态结构图
图1:把服务端文件映射为唯一的相对 ZIP 条目名,再交给 archive/zip Writer。
type ZipItem struct {
    Path string // 服务端实际文件
    Name string // ZIP 内部名称,例如 reports/report.pdf
}

items := []ZipItem{
    {Path: "/srv/reports/today.pdf", Name: "reports/today.pdf"},
    {Path: "/srv/export/data.csv", Name: "data/data.csv"},
    {Path: "/srv/export/README.txt", Name: "README.txt"},
}

Name 应由服务端生成或经过严格校验,至少保证非空、使用正斜杠、不是绝对路径,也不包含 ../。如果两个文件使用同一个条目名,解压工具的表现并不适合作为业务规则,最好在进入打包函数前就拒绝重复名称。

用 archive/zip 逐个写入文件内容

下面的函数只负责创建归档。它使用临时文件承接结果,所以大文件不会整体驻留在内存中;每次循环只打开一个源文件,复制完成后立即关闭。

func buildZip(items []ZipItem) (string, error) {
    f, err := os.CreateTemp("", "download-*.zip")
    if err != nil {
        return "", err
    }
    path := f.Name()
    remove := true
    defer func() {
        _ = f.Close()
        if remove {
            _ = os.Remove(path)
        }
    }()

    zw := zip.NewWriter(f)
    for _, item := range items {
        src, err := os.Open(item.Path)
        if err != nil {
            return "", fmt.Errorf("open %s: %w", item.Path, err)
        }
        dst, err := zw.Create(item.Name)
        if err == nil {
            _, err = io.Copy(dst, src)
        }
        _ = src.Close()
        if err != nil {
            return "", fmt.Errorf("add %s: %w", item.Name, err)
        }
    }
    if err := zw.Close(); err != nil {
        return "", fmt.Errorf("close zip: %w", err)
    }
    if err := f.Close(); err != nil {
        return "", fmt.Errorf("close file: %w", err)
    }
    remove = false
    return path, nil
}

这里的关闭顺序不能交换:先关闭 zip.Writer,让它写入 ZIP 的中央目录,再关闭底层文件。官方文档也明确说明,Writer.Close 只结束 ZIP 写入,并不会替你关闭底层 writer。

归档完成后再交给 HTTP 下载

有了完整的临时 ZIP,Handler 才设置响应头并复制文件。这样源文件缺失、归档写入失败时,还能返回清晰的 HTTP 错误,而不是先发出 200 后再传一个半成品。

Go ZIP 下载中临时文件、中央目录、HTTP 响应头与失败分支的静态关系图
图2:先完成 ZIP 的中央目录,再发送 attachment 响应,避免把半成品当成成功下载。
func downloadZip(w http.ResponseWriter, r *http.Request) {
    items, err := loadAllowedItems(r) // 只返回服务端白名单文件
    if err != nil {
        http.Error(w, "file list is invalid", http.StatusBadRequest)
        return
    }

    zipPath, err := buildZip(items)
    if err != nil {
        http.Error(w, "build zip failed", http.StatusInternalServerError)
        return
    }
    defer os.Remove(zipPath)

    f, err := os.Open(zipPath)
    if err != nil {
        http.Error(w, "open zip failed", http.StatusInternalServerError)
        return
    }
    defer f.Close()

    info, err := f.Stat()
    if err != nil {
        http.Error(w, "stat zip failed", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/zip")
    w.Header().Set("Content-Disposition", `attachment; filename="reports.zip"`)
    http.ServeContent(w, r, "reports.zip", info.ModTime(), f)
}

http.ServeContent 能根据文件信息处理内容长度和范围请求;如果业务不需要断点续传,也可以在设置响应头后使用 io.Copy。下载文件名是固定的,避免直接把请求参数拼进响应头。

一次故障复盘:为什么下载得到的是损坏 ZIP

这类问题通常有一条很短的时间线:Handler 创建了 zip.Writer,循环里的文件也都复制成功;随后代码直接关闭了临时文件,却漏掉 zw.Close()。客户端能够收到字节,却在解压时提示“压缩包损坏”。根因不是网络,也不是文件内容,而是 ZIP 的中央目录尚未写出。

修复动作有三个:把 zw.Close() 放在底层文件关闭之前;把每次 OpenCreateio.Copy 和两个 Close 的错误都返回;只有 buildZip 成功后才写入 HTTP 200 响应。防复发时,再为条目名做唯一性检查,并用固定白名单生成 ZipItem,不要让用户直接决定服务器要打开哪个路径。

常见问题

为什么不直接用 bytes.Buffer?

小文件集合可以这样做,但归档大小会随文件总量增长并占用进程内存。临时文件方案更适合报告、导出数据等体积不确定的下载。

文件名包含中文会不会乱码?

archive/zip 会按 ZIP 规则处理有效 UTF-8 名称;仍建议统一使用 UTF-8 和正斜杠,并用常见解压工具做兼容性测试。

能不能把用户上传的路径直接写进 ZIP?

不建议。物理路径和条目名都应由服务端约束,拒绝绝对路径、盘符路径、反斜杠和 ../,并限制可访问的根目录。

把多个文件打包成 ZIP 的核心并不复杂:明确文件清单,逐个复制到 zip.Writer,检查 Close,归档完成后再下载。真正容易出故障的地方,是把条目命名、错误处理和 HTTP 响应时机当成了细节。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
兽音译者解码失败怎么办?字符设置、复制与本地处理说明兽音译者解码失败怎么办?字符设置、复制与本地处理说明
上一篇
兽音译者解码失败怎么办?字符设置、复制与本地处理说明
特效变音魔术师怎么设置铃声和通知音?音质、暗色模式与权限说明
下一篇
特效变音魔术师怎么设置铃声和通知音?音质、暗色模式与权限说明
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    145次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    68次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    35次使用
  • PromptHero官网:AI提示词搜索、优化与学习平台,支持Midjourney/Stable Diffusion
    PromptHero
    PromptHero是专业的AI提示词搜索引擎与优化平台,支持Stable Diffusion、Midjourney等主流模型。提供海量提示词库、分类搜索、在线课程及社区互动,助力用户高效生成高质量AI图像与文本。
    10次使用
  • OpenArt免费开源指南:Stable Diffusion Prompt Book提示词手册详解
    Stable Diffusion Prompt Book
    深入解析OpenArt推出的Stable Diffusion Prompt Book,这本免费的开源提示词指南涵盖从基础语法到高级技巧,提供风格化词库与参数建议,助您优化AI绘画生成效果。
    21次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码