Go语言中数据压缩与解压的实现

作者:

文章目录
  • Gzip 是目前最流行的文件压缩格式之一,广泛用于 Web 服务器(内容编码 gzip)和文件压缩。
  • Zlib 格式与 Gzip 使用相同的 DEFLATE 压缩算法,但它的头部和尾部格式不同,设计更紧凑,常用于网络协议中的数据流压缩(例如在 HTTP 的 Content-Encoding: deflate 中,尽管实现上有些混乱,但 zlib 是其意图)。使用方式与 Gzip 几乎完全一样,只是换成了 compress/zlib 包。
  • .zip 文件是一个归档格式,它可以包含多个文件和目录,并且通常会对每个文件进行单独压缩。Go 的 archive/zip 包提供了创建和读取 zip 文件的功能。
  • tar (Tape Archive) 本身不是一个压缩格式,而是一个归档格式,它将多个文件打包成一个单一的 .tar 文件,但不进行压缩。因此,.tar 文件通常会和压缩工具结合使用,最常见的就是 tar.gz (或 .tgz),即先用 tar 打包,再用 gzip 压缩。Go 的 archive/tar 包用于处理 tar 格式。
  • 目录
    • 一、核心概念
      • 1.1 压缩与解压类型
      • 1.2io.Reader和io.Writer的魔力
      • 1.3 使用建议
    • 二、Gzip 压缩与解压
      • 2.1 案例:Gzip 压缩文件
      • 2.2 案例:Gzip 解压文件
    • 三、Zlib 压缩与解压
      • 3.1 案例:Zlib 压缩与解压(内存中操作)
    • 四、Zip 归档与压缩
      • 4.1 案例:创建一个 Zip 文件
      • 4.2 案例:解压一个 Zip 文件
    • 五、Tar 归档(配合 Gzip)
      • 5.1 案例:创建一个 Tar.gz 文件
      • 5.2 案例:解压一个 Tar.gz 文件

    Go 语言的标准库 compress 提供了对多种常见压缩格式的支持,包括 gzipzlibflatebzip2。此外,虽然 ziptar 更像是归档格式,但它们通常也和压缩紧密相关,因此我们也会一并介绍。
    本文将遵循以下结构:

    1. 核心概念:理解 Go 中压缩/解压的通用工作流。
    2. Gzip 压缩与解压:最常用的压缩格式之一,常用于 HTTP 压缩和文件压缩。
    3. Zlib 压缩与解压:与 Gzip 关系密切,常用于网络数据流压缩。
    4. Zip 归档与压缩:创建和解压 .zip 文件,这是最通用的归档格式之一。
    5. Tar 归档(配合 Gzip):在 Linux/Unix 世界中,tar.gz 是标准的分发格式。

    在 Go 中处理压缩/解压时,理解 io.Readerio.Writer 接口至关重要。Go 的压缩库设计得非常巧妙,它将压缩/解压逻辑包装成了一个实现了 io.Readerio.Writer 接口的“过滤器”。

    • 压缩:你创建一个压缩写入器(如 gzip.NewWriter),它接受一个普通的 io.Writer(如文件或内存缓冲区)。然后,你向这个压缩写入器写入未压缩的数据,它会自动将数据压缩后传递给底层的 io.Writer
    • 解压:你创建一个解压读取器(如 gzip.NewReader),它接受一个 io.Reader(如一个压缩文件)。然后,你从这个解压读取器中读取数据,它会自动从底层 io.Reader 读取压缩数据,解压后提供给你。
      这种设计使得压缩/解压操作可以无缝地与文件、网络连接、内存缓冲区等任何实现了 io.Reader/Writer 的对象协同工作,体现了 Go 的组合哲学。
      通用工作流:
      压缩:
    1. 创建一个目标 io.Writer (例如 os.Create 创建的文件)。
    2. 使用目标 io.Writer 创建一个压缩写入器 (例如 gzip.NewWriter)。
    3. 将原始数据写入压缩写入器。
    4. 关键一步:调用压缩写入器的 Close() 方法。这会刷新所有内部缓冲区,并将压缩流的尾部数据写入目标 io.Writer。忘记 Close() 会导致生成的压缩文件不完整或损坏。
      解压:
    5. 创建一个源 io.Reader (例如 os.Open 打开的压缩文件)。
    6. 使用源 io.Reader 创建一个解压读取器 (例如 gzip.NewReader)。
    7. 从解压读取器中读取数据,得到的就是解压后的原始数据。
    8. (可选)关闭解压读取器,以释放底层资源。

    1. 选择合适的格式:
      • Gzip: 通用文件压缩,Web 压缩。压缩率和速度平衡得很好。
      • Zlib: 网络数据流压缩。头部比 Gzip 小,非常适合在通信协议中使用。
      • Zip: 跨平台归档。Windows 和 macOS 都有原生支持,方便分发。
      • Tar.gz: Linux/Unix 世界的标准分发格式。适合打包整个项目目录,保留文件权限和符号链接等信息。
    2. 压缩级别:
      • gzipzlib 包允许你设置压缩级别,通过 &gzip.Writer{Level: gzip.BestCompression} 这样的方式创建 writer。
      • gzip.DefaultCompression 是默认值,通常是速度和压缩率的最佳平衡点。
      • gzip.BestSpeed: 压缩速度最快,但压缩率最低。
      • gzip.BestCompression: 压缩率最高,但速度最慢,CPU 占用最多。
      • gzip.NoCompression: 仅打包,不压缩。
      • 建议: 对于大多数服务器端应用,默认级别就足够了。在资源受限的嵌入式设备或对启动速度要求极高的场景,可以考虑 BestSpeed。对于一次性归档且不关心时间的场景,可以使用 BestCompression
    3. 内存使用:
      • io.Copy 非常高效,它内部使用了一个 32KB 的缓冲区,避免了将整个文件加载到内存中。这使得 Go 可以轻松处理比可用内存大得多的文件。
      • 如果你在处理大量小文件,频繁创建和关闭 gzip.Writer 可能会有开销。可以考虑复用 writer(如果场景允许)。
    4. 错误处理:
      • 始终检查 Close() 方法返回的错误。在写入操作中,Close() 是将缓冲区数据刷入底层 io.Writer 的最后机会,也是最容易出错的地方。
      • 使用 defer 来确保资源(文件、reader、writer)被关闭,但要记住 defer 的错误处理。如果 Close() 的错误很重要,最好在函数末尾显式处理它,而不是依赖 defer
    5. 安全性:
      • 如在 Zip 和 Tar 解压示例中所示,永远不要信任来自外部的文件路径。在解压前,务必验证文件路径是否是合法的,防止路径遍历攻击。

    Gzip 是目前最流行的文件压缩格式之一,广泛用于 Web 服务器(内容编码 gzip)和文件压缩。

    我们将创建一个程序,将一个文本文件 original.txt 压缩成 original.txt.gz

    // main.go
    package main
    import (
    	"compress/gzip"
    	"fmt"
    	"io"
    	"log"
    	"os"
    )
    func main() {
    	// 1. 准备源文件和目标文件
    	sourceFile, err := os.Open("original.txt")
    	if err != nil {
    		log.Fatalf("Failed to open source file: %v", err)
    	}
    	defer sourceFile.Close()
    	destFile, err := os.Create("original.txt.gz")
    	if err != nil {
    		log.Fatalf("Failed to create destination file: %v", err)
    	}
    	// 使用 defer 确保文件在函数结束时关闭
    	defer destFile.Close()
    	// 2. 创建一个 gzip.Writer,它将压缩数据写入 destFile
    	gzipWriter := gzip.NewWriter(destFile)
    	// 使用 defer 确保在所有数据写入后,关闭 gzip.Writer,这会写入尾部信息
    	defer gzipWriter.Close()
    	// 3. 将源文件内容拷贝到 gzip.Writer
    	// io.Copy 会高效地处理数据流的拷贝
    	bytesWritten, err := io.Copy(gzipWriter, sourceFile)
    	if err != nil {
    		log.Fatalf("Failed to compress data: %v", err)
    	}
    	fmt.Printf("Successfully compressed. Original size: ~%d bytes, written to gzip writer: %d bytes.n", bytesWritten, bytesWritten)
    	// 注意:gzipWriter 内部会缓冲,实际写入 destFile 的大小会小于 bytesWritten
    }
    

    运行前准备:
    创建一个 original.txt 文件,并填入一些内容,例如:

    Hello, Go!
    This is a test file for gzip compression.
    It contains multiple lines to demonstrate the process.
    Go's standard library makes compression a breeze.

    运行

    go run main.go
    

    运行后,你会发现目录下多了一个 original.txt.gz 文件。你可以使用系统命令(如 gunzip original.txt.gz 或在图形界面中解压)来验证它是否正确。

    现在,我们将刚才创建的 original.txt.gz 文件解压出来。

    // main.go
    package main
    import (
    	"compress/gzip"
    	"fmt"
    	"io"
    	"log"
    	"os"
    )
    func main() {
    	// 1. 打开压缩的源文件
    	gzipFile, err := os.Open("original.txt.gz")
    	if err != nil {
    		log.Fatalf("Failed to open gzip file: %v", err)
    	}
    	defer gzipFile.Close()
    	// 2. 创建一个 gzip.Reader,它会从 gzipFile 读取并解压数据
    	// 注意:NewReader 返回的 reader 也需要关闭,以释放资源
    	gzipReader, err := gzip.NewReader(gzipFile)
    	if err != nil {
    		log.Fatalf("Failed to create gzip reader: %v", err)
    	}
    	defer gzipReader.Close()
    	// 3. 创建解压后的目标文件
    	destFile, err := os.Create("unzipped.txt")
    	if err != nil {
    		log.Fatalf("Failed to create destination file: %v", err)
    	}
    	defer destFile.Close()
    	// 4. 将解压后的数据从 gzipReader 拷贝到目标文件
    	bytesRead, err := io.Copy(destFile, gzipReader)
    	if err != nil {
    		log.Fatalf("Failed to decompress data: %v", err)
    	}
    	fmt.Printf("Successfully decompressed. Read %d bytes from gzip stream, written to unzipped.txt.n", bytesRead)
    }
    

    运行:

    go run main.go
    

    运行后,你会得到一个 unzipped.txt 文件,其内容与最初的 original.txt 完全一致。

    Zlib 格式与 Gzip 使用相同的 DEFLATE 压缩算法,但它的头部和尾部格式不同,设计更紧凑,常用于网络协议中的数据流压缩(例如在 HTTP 的 Content-Encoding: deflate 中,尽管实现上有些混乱,但 zlib 是其意图)。
    使用方式与 Gzip 几乎完全一样,只是换成了 compress/zlib 包。

    这个例子将展示如何在内存中对一个字节切片进行压缩和解压,这在处理网络数据或缓存时非常常见。

    // main.go
    package main
    import (
    	"bytes"
    	"compress/zlib"
    	"fmt"
    	"io"
    )
    func main() {
    	originalData := []byte("This is some data that we will compress using zlib in memory. " +
    		"It's a very common use case for network communications.")
    	fmt.Printf("Original size: %d bytesn", len(originalData))
    	fmt.Println("Original data:", string(originalData))
    	// --- 压缩 ---
    	var compressedBuffer bytes.Buffer
    	zlibWriter := zlib.NewWriter(&compressedBuffer)
    	_, err := zlibWriter.Write(originalData)
    	if err != nil {
    		panic(err)
    	}
    	// 关闭 writer 以刷新缓冲区
    	zlibWriter.Close()
    	compressedData := compressedBuffer.Bytes()
    	fmt.Printf("Compressed size: %d bytesn", len(compressedData))
    	// --- 解压 ---
    	// 从压缩后的字节切片创建一个 reader
    	zlibReader, err := zlib.NewReader(bytes.NewReader(compressedData))
    	if err != nil {
    		panic(err)
    	}
    	defer zlibReader.Close()
    	var decompressedBuffer bytes.Buffer
    	// 将解压后的数据拷贝到新的缓冲区
    	_, err = io.Copy(&decompressedBuffer, zlibReader)
    	if err != nil {
    		panic(err)
    	}
    	decompressedData := decompressedBuffer.Bytes()
    	fmt.Printf("Decompressed size: %d bytesn", len(decompressedData))
    	fmt.Println("Decompressed data:", string(decompressedData))
    	// 验证
    	if bytes.Equal(originalData, decompressedData) {
    		fmt.Println("nSuccess! Original and decompressed data match.")
    	} else {
    		fmt.Println("nError! Data does not match.")
    	}
    }
    

    .zip 文件是一个归档格式,它可以包含多个文件和目录,并且通常会对每个文件进行单独压缩。Go 的 archive/zip 包提供了创建和读取 zip 文件的功能。

    我们将把两个文件 file1.txtfile2.txt 打包到 archive.zip 中。
    运行前准备:

    echo "Content of file one." > file1.txt
    echo "Content of file two, which is slightly longer." > file2.txt
    
    // main.go
    package main
    import (
    	"archive/zip"
    	"io"
    	"log"
    	"os"
    )
    func main() {
    	// 1. 创建 zip 文件
    	zipFile, err := os.Create("archive.zip")
    	if err != nil {
    		log.Fatalf("Failed to create zip file: %v", err)
    	}
    	defer zipFile.Close()
    	// 2. 创建一个 zip.Writer
    	zipWriter := zip.NewWriter(zipFile)
    	defer zipWriter.Close() // 关闭 writer 以写入 zip 的中央目录记录
    	// 3. 定义要添加的文件列表
    	filesToAdd := []string{"file1.txt", "file2.txt"}
    	for _, filename := range filesToAdd {
    		// 3.1. 在 zip 文件中创建一个文件头
    		// 这相当于在 zip 内部创建一个空的文件结构
    		writer, err := zipWriter.Create(filename)
    		if err != nil {
    			log.Fatalf("Failed to create entry for %s in zip: %v", filename, err)
    		}
    		// 3.2. 打开要添加的原始文件
    		file, err := os.Open(filename)
    		if err != nil {
    			log.Fatalf("Failed to open %s: %v", filename, err)
    		}
    		defer file.Close()
    		// 3.3. 将原始文件内容拷贝到 zip 内部的文件 writer 中
    		_, err = io.Copy(writer, file)
    		if err != nil {
    			log.Fatalf("Failed to write %s to zip: %v", filename, err)
    		}
    		log.Printf("Added %s to archive.zipn", filename)
    	}
    	log.Println("Successfully created archive.zip")
    }
    

    现在,我们将 archive.zip 解压到一个名为 unzipped_archive 的目录中。

    // main.go
    package main
    import (
    	"archive/zip"
    	"io"
    	"log"
    	"os"
    	"path/filepath"
    )
    func main() {
    	// 1. 打开 zip 文件
    	zipReader, err := zip.OpenReader("archive.zip")
    	if err != nil {
    		log.Fatalf("Failed to open zip file: %v", err)
    	}
    	defer zipReader.Close()
    	// 2. 创建解压目标目录
    	destDir := "unzipped_archive"
    	err = os.MkdirAll(destDir, 0755)
    	if err != nil {
    		log.Fatalf("Failed to create destination directory: %v", err)
    	}
    	// 3. 遍历 zip 文件中的每一个文件/目录
    	for _, f := range zipReader.File {
    		// 3.1. 构造解压后的完整文件路径
    		// filepath.Join 会处理不同操作系统的路径分隔符
    		destPath := filepath.Join(destDir, f.Name)
    		// 安全检查:防止 ZipSlip 漏洞(路径遍历攻击)
    		// 确保 f.Name 不会跳出目标目录
    		if !strings.HasPrefix(destPath, filepath.Clean(destDir)+string(os.PathSeparator)) {
    			log.Fatalf("Invalid file path: %s", f.Name)
    		}
    		log.Printf("Extracting %s to %s", f.Name, destPath)
    		// 3.2. 如果是目录,则创建它
    		if f.FileInfo().IsDir() {
    			os.MkdirAll(destPath, f.Mode())
    			continue
    		}
    		// 3.3. 如果是文件,则创建它并写入内容
    		// 确保文件的父目录存在
    		os.MkdirAll(filepath.Dir(destPath), 0755)
    		// 打开 zip 内的文件
    		rc, err := f.Open()
    		if err != nil {
    			log.Fatalf("Failed to open file %s in zip: %v", f.Name, err)
    		}
    		// 创建目标文件
    		destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
    		if err != nil {
    			rc.Close()
    			log.Fatalf("Failed to create destination file %s: %v", destPath, err)
    		}
    		// 拷贝文件内容
    		_, err = io.Copy(destFile, rc)
    		rc.Close()
    		destFile.Close()
    		if err != nil {
    			log.Fatalf("Failed to write file %s: %v", destPath, err)
    		}
    	}
    	log.Println("Successfully extracted archive.zip to unzipped_archive directory.")
    }
    

    注意: 在解压代码中,我们加入了一个重要的安全检查来防止 ZipSlip 漏洞。这是一个常见的漏洞,恶意制作的 zip 文件可能包含如 ../../../evil.sh 这样的路径,如果解压程序不加检查,可能会覆盖系统中的重要文件。我们的检查确保所有解压的文件都位于目标目录 destDir 内。

    tar (Tape Archive) 本身不是一个压缩格式,而是一个归档格式,它将多个文件打包成一个单一的 .tar 文件,但不进行压缩。因此,.tar 文件通常会和压缩工具结合使用,最常见的就是 tar.gz (或 .tgz),即先用 tar 打包,再用 gzip 压缩。
    Go 的 archive/tar 包用于处理 tar 格式。

    这个过程是两步的:创建一个 tar writer,然后将它包装在一个 gzip writer 中。
    运行前准备:

    mkdir myproject
    echo "package mainnnfunc main() {ntprintln("Hello from main.go")n}" > myproject/main.go
    echo "module myprojectnngo 1.21" > myproject/go.mod
    
    // main.go
    package main
    import (
    	"archive/tar"
    	"compress/gzip"
    	"io"
    	"log"
    	"os"
    	"path/filepath"
    )
    func main() {
    	// 1. 创建最终的 .tar.gz 文件
    	tarGzFile, err := os.Create("myproject.tar.gz")
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer tarGzFile.Close()
    	// 2. 创建 gzip writer,它将数据写入 tarGzFile
    	gzipWriter := gzip.NewWriter(tarGzFile)
    	defer gzipWriter.Close()
    	// 3. 创建 tar writer,它将数据写入 gzipWriter
    	tarWriter := tar.NewWriter(gzipWriter)
    	defer tarWriter.Close()
    	// 4. 遍历 "myproject" 目录,将文件添加到 tar 归档中
    	sourceDir := "myproject"
    	err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
    		if err != nil {
    			return err
    		}
    		// 创建 tar 头部信息
    		header, err := tar.FileInfoHeader(info, info.Name())
    		if err != nil {
    			return err
    		}
    		// 调整头部中的 Name,使其为相对于源目录的路径
    		relPath, err := filepath.Rel(sourceDir, path)
    		if err != nil {
    			return err
    		}
    		header.Name = relPath
    		// 写入头部
    		if err := tarWriter.WriteHeader(header); err != nil {
    			return err
    		}
    		// 如果是普通文件,则写入文件内容
    		if !info.Mode().IsRegular() {
    			return nil
    		}
    		file, err := os.Open(path)
    		if err != nil {
    			return err
    		}
    		defer file.Close()
    		_, err = io.Copy(tarWriter, file)
    		return err
    	})
    	if err != nil {
    		log.Fatal(err)
    	}
    	log.Println("Successfully created myproject.tar.gz")
    }
    

    这个过程也是两步的:先用 gzip reader 解压,然后用 tar reader 解包。

    // main.go
    package main
    import (
    	"archive/tar"
    	"compress/gzip"
    	"io"
    	"log"
    	"os"
    	"path/filepath"
    )
    func main() {
    	// 1. 打开 .tar.gz 文件
    	tarGzFile, err := os.Open("myproject.tar.gz")
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer tarGzFile.Close()
    	// 2. 创建 gzip reader
    	gzipReader, err := gzip.NewReader(tarGzFile)
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer gzipReader.Close()
    	// 3. 创建 tar reader
    	tarReader := tar.NewReader(gzipReader)
    	// 4. 创建解压目标目录
    	destDir := "extracted_project"
    	os.MkdirAll(destDir, 0755)
    	// 5. 遍历 tar 归档中的文件
    	for {
    		header, err := tarReader.Next()
    		if err == io.EOF {
    			break // 文件结束
    		}
    		if err != nil {
    			log.Fatal(err)
    		}
    		// 构造目标路径
    		destPath := filepath.Join(destDir, header.Name)
    		// 安全检查:防止路径遍历
    		if !strings.HasPrefix(destPath, filepath.Clean(destDir)+string(os.PathSeparator)) {
    			log.Fatalf("Invalid file path: %s", header.Name)
    		}
    		log.Printf("Extracting %s to %s", header.Name, destPath)
    		switch header.Typeflag {
    		case tar.TypeDir:
    			// 如果是目录,创建它
    			if err := os.MkdirAll(destPath, os.FileMode(header.Mode)); err != nil {
    				log.Fatal(err)
    			}
    		case tar.TypeReg:
    			// 如果是文件,创建它并写入内容
    			outFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
    			if err != nil {
    				log.Fatal(err)
    			}
    			if _, err := io.Copy(outFile, tarReader); err != nil {
    				outFile.Close()
    				log.Fatal(err)
    			}
    			outFile.Close()
    		}
    	}
    	log.Println("Successfully extracted myproject.tar.gz to extracted_project directory.")
    }
    

    总结:Go 语言的 compressarchive 标准库为数据压缩和归档提供了强大而灵活的工具。通过 io.Readerio.Writer 接口,这些工具可以无缝地集成到各种 I/O 场景中。

    • 对于简单压缩,使用 compress/gzipcompress/zlib
    • 对于跨平台归档,使用 archive/zip
    • 对于类 Unix 系统的打包分发,组合使用 archive/tarcompress/gzip

    掌握这些库的使用,将使你能够轻松处理文件存储、网络传输、数据备份等常见任务。记住核心的工作流、善用 defer、注意错误处理和安全性,才能写出健壮且高效的 Go 压缩/解压程序。

    到此这篇关于Go语言中数据压缩与解压的实现的文章就介绍到这了,更多相关Go语言数据压缩与解压内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

    您可能感兴趣的文章:

    • Golang利用compress/flate包来压缩和解压数据
    • golang压缩与解压缩文件的示例代码
    • Golang使用archive/zip包实现ZIP压缩与解压
    • Go实现自动解压缩包以及读取docx/doc文件内容详解
    • go 压缩解压zip文件源码示例
    • golang中tar压缩和解压文件详情
    • Golang使用zlib压缩和解压缩字符串
    • MongoDB windows解压缩版安装教程详解
    • Go语言压缩和解压缩tar.gz文件的方法

    站内搜索