Go 怎么把多个文件打包成 ZIP 并提供下载
来源:17golang原创
时间:2026-09-05 12:12:44 316浏览 收藏
接口原本只返回一个文件,后来需求变成“把报告、数据和说明一起下载”。最省事的做法不是先把所有文件读进 bytes.Buffer,而是使用标准库 archive/zip,把文件逐个写入 ZIP,再以附件响应返回。需要特别记住两点:ZIP 条目名要自己规划,zip.Writer.Close() 必须成功调用,否则中央目录没有写完,客户端拿到的往往只是损坏压缩包。
- 用
zip.NewWriter创建归档,Create创建每个内部文件。 - 使用相对且唯一的条目名,不能把用户传入路径原样写入 ZIP。
- 先完成并关闭 ZIP,再设置下载响应;中途失败要清理临时文件。
先把文件列表变成稳定的 ZIP 条目
先把“物理路径”和“压缩包内名称”分开。物理路径用于服务端打开文件,条目名只描述下载者解压后看到的目录结构。这样既能保留报告目录,也不会把服务器绝对路径暴露给客户端。

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 后再传一个半成品。

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() 放在底层文件关闭之前;把每次 Open、Create、io.Copy 和两个 Close 的错误都返回;只有 buildZip 成功后才写入 HTTP 200 响应。防复发时,再为条目名做唯一性检查,并用固定白名单生成 ZipItem,不要让用户直接决定服务器要打开哪个路径。
常见问题
为什么不直接用 bytes.Buffer?
小文件集合可以这样做,但归档大小会随文件总量增长并占用进程内存。临时文件方案更适合报告、导出数据等体积不确定的下载。
文件名包含中文会不会乱码?
archive/zip 会按 ZIP 规则处理有效 UTF-8 名称;仍建议统一使用 UTF-8 和正斜杠,并用常见解压工具做兼容性测试。
能不能把用户上传的路径直接写进 ZIP?
不建议。物理路径和条目名都应由服务端约束,拒绝绝对路径、盘符路径、反斜杠和 ../,并限制可访问的根目录。
把多个文件打包成 ZIP 的核心并不复杂:明确文件清单,逐个复制到 zip.Writer,检查 Close,归档完成后再下载。真正容易出故障的地方,是把条目命名、错误处理和 HTTP 响应时机当成了细节。
-
254 收藏
-
171 收藏
-
423 收藏
-
339 收藏
-
142 收藏
-
462 收藏
-
189 收藏
-
409 收藏
-
151 收藏
-
314 收藏
-
445 收藏
-
311 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习