登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 条目

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

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>