压缩和归档是很多后台工具的基础能力
服务端程序经常需要处理文件:日志按天压缩,报表打包下载,备份目录归档,上传的 zip 解开检查。Go 标准库提供了 compress/gzip、archive/tar、archive/zip 等包,足够覆盖很多入门和中小项目需求。
先分清两个概念:压缩是让数据变小,比如 gzip;归档是把多个文件组织成一个文件,比如 tar。.tar.gz 通常表示先 tar 归档,再 gzip 压缩。zip 则同时包含归档和压缩能力。
这篇文章用日志压缩和目录打包做例子,讲清楚基本数据流。重点仍然是 io.Reader、io.Writer 的组合。
gzip 压缩字符串或文件
压缩到 writer:
func GzipData(w io.Writer, data []byte) error {
gz := gzip.NewWriter(w)
defer gz.Close()
if _, err := gz.Write(data); err != nil {
return fmt.Errorf("write gzip: %w", err)
}
return nil
}
使用:
var buf bytes.Buffer
if err := GzipData(&buf, []byte("hello hello hello")); err != nil {
return err
}
fmt.Println(buf.Len())
压缩文件:
func GzipFile(dstPath, srcPath string) error {
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer src.Close()
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("create gzip file: %w", err)
}
defer dst.Close()
gz := gzip.NewWriter(dst)
defer gz.Close()
if _, err := io.Copy(gz, src); err != nil {
return fmt.Errorf("copy gzip data: %w", err)
}
return nil
}
gz.Close() 很重要,它会写入压缩尾部信息。忘记关闭可能得到损坏文件。
gzip 解压
func Gunzip(r io.Reader) ([]byte, error) {
gz, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("create gzip reader: %w", err)
}
defer gz.Close()
data, err := io.ReadAll(gz)
if err != nil {
return nil, fmt.Errorf("read gzip: %w", err)
}
return data, nil
}
如果输入来自不可信来源,解压时要考虑大小限制。压缩文件可能很小,解开后很大。可以用 io.LimitReader 或限制写入目标。
tar 归档多个文件
创建 tar:
func WriteTar(w io.Writer, files map[string]string) error {
tw := tar.NewWriter(w)
defer tw.Close()
for name, content := range files {
data := []byte(content)
header := &tar.Header{
Name: name,
Mode: 0644,
Size: int64(len(data)),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("write tar header: %w", err)
}
if _, err := tw.Write(data); err != nil {
return fmt.Errorf("write tar body: %w", err)
}
}
return nil
}
组合成 .tar.gz:
func WriteTarGz(w io.Writer, files map[string]string) error {
gz := gzip.NewWriter(w)
defer gz.Close()
return WriteTar(gz, files)
}
这个组合很能体现 Go 的数据流思路:tar 写到 gzip,gzip 再写到最终 writer。每层只关心自己的接口。
zip 打包
func WriteZip(w io.Writer, files map[string]string) error {
zw := zip.NewWriter(w)
defer zw.Close()
for name, content := range files {
fileWriter, err := zw.Create(name)
if err != nil {
return fmt.Errorf("create zip entry: %w", err)
}
if _, err := io.WriteString(fileWriter, content); err != nil {
return fmt.Errorf("write zip entry: %w", err)
}
}
return nil
}
写到 HTTP 响应:
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", `attachment; filename="report.zip"`)
err := WriteZip(w, files)
zip 很适合下载多个报表文件。文件名要小心,不要直接信任用户输入的路径,避免生成奇怪目录结构。
小结
Go 标准库已经提供了常见压缩和归档能力。gzip 负责压缩单个数据流,tar 负责把多个文件归档,zip 同时处理归档和压缩。它们都围绕 io.Reader 和 io.Writer 设计,可以自然组合。
处理压缩文件时,要记得关闭 writer,给错误加上下文,面对不可信输入时限制解压大小和文件路径。只要把数据流想清楚,Go 写这类后台工具会很顺手。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。