5. 高级应用
这里记录两种做法:用 Goroutine 并行压缩和解压多个文件,以及用流式读写处理大文件,避免把整个文件读进内存。
5.1 并行压缩和解压
文件多的时候,可以起多个 goroutine 同时做压缩和解压。
5.1.1 使用 Goroutines 进行并行处理
并行压缩多个文件:
go
import (
"archive/zip"
"os"
"io"
"path/filepath"
"sync"
)
func createZipParallel(outputFile string, files []string) error {
outFile, err := os.Create(outputFile)
if err != nil {
return err
}
defer outFile.Close()
zipWriter := zip.NewWriter(outFile)
defer zipWriter.Close()
var wg sync.WaitGroup
var mu sync.Mutex // 注意:zip.Writer 不是并发安全的,写入必须用锁串行化
errChan := make(chan error, len(files))
for _, file := range files {
wg.Add(1)
go func(filePath string) {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
if err := addFileToZip(zipWriter, filePath); err != nil {
errChan <- err
}
}(file)
}
wg.Wait()
close(errChan)
if len(errChan) > 0 {
return <-errChan
}
return nil
}
func addFileToZip(zipWriter *zip.Writer, filePath string) error {
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
w, err := zipWriter.Create(filepath.Base(filePath))
if err != nil {
return err
}
_, err = io.Copy(w, f)
return err
}注意:
zip.Writer本身不是并发安全的,多个 goroutine 直接并发调用会产生数据竞争(可用go run -race验证)。上面的实现通过互斥锁保证同一时刻只有一个 goroutine 在写入;如需真正并行的压缩计算,可以先并行读取并压缩到内存,再串行写入zip.Writer。
并行解压多个文件:
go
func extractZipParallel(inputFile, outputDir string) error {
zipReader, err := zip.OpenReader(inputFile)
if err != nil {
return err
}
defer zipReader.Close()
var wg sync.WaitGroup
errChan := make(chan error, len(zipReader.File))
for _, file := range zipReader.File {
wg.Add(1)
go func(f *zip.File) {
defer wg.Done()
if err := extractFileFromZip(f, outputDir); err != nil {
errChan <- err
}
}(file)
}
wg.Wait()
close(errChan)
if len(errChan) > 0 {
return <-errChan
}
return nil
}
func extractFileFromZip(file *zip.File, outputDir string) error {
outputFile := filepath.Join(outputDir, file.Name)
if file.FileInfo().IsDir() {
return os.MkdirAll(outputFile, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(outputFile), os.ModePerm); err != nil {
return err
}
outFile, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
defer outFile.Close()
rc, err := file.Open()
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(outFile, rc)
return err
}5.1.2 性能优化
并行化之后有三处值得调整:把小块读写合并成批量 I/O,减少磁盘往返;给 goroutine 的数量设上限,避免开太多导致内存占用过高;读写走缓冲区,降低小块数据带来的开销。
5.1.3 实践案例
假设我们有多个大文件需要并行压缩成 archive.zip,然后解压到 output 目录。
go
func main() {
files := []string{"file1.txt", "file2.txt", "file3.txt"}
zipFile := "archive.zip"
outputDir := "output"
// 并行创建 zip 文件
err := createZipParallel(zipFile, files)
if err != nil {
log.Fatal(err)
}
fmt.Println("Parallel zip file created successfully")
// 并行解压 zip 文件
err = extractZipParallel(zipFile, outputDir)
if err != nil {
log.Fatal(err)
}
fmt.Println("Parallel zip file extracted successfully")
}运行此代码后,我们会看到控制台输出:
plaintext
Parallel zip file created successfully
Parallel zip file extracted successfully并且在当前目录下生成 archive.zip 文件和 output 目录,其中包含多个文件。
5.2 大文件处理
处理大文件的关键是不要一次性把它读进内存,流式读写配合分块处理即可。
5.2.1 流式处理大文件
压缩大文件:
go
import (
"compress/gzip"
"os"
"io"
)
func compressLargeFile(inputFile, outputFile string) error {
inFile, err := os.Open(inputFile)
if err != nil {
return err
}
defer inFile.Close()
outFile, err := os.Create(outputFile)
if err != nil {
return err
}
defer outFile.Close()
gzipWriter := gzip.NewWriter(outFile)
defer gzipWriter.Close()
_, err = io.Copy(gzipWriter, inFile)
return err
}解压大文件:
go
func decompressLargeFile(inputFile, outputFile string) error {
inFile, err := os.Open(inputFile)
if err != nil {
return err
}
defer inFile.Close()
gzipReader, err := gzip.NewReader(inFile)
if err != nil {
return err
}
defer gzipReader.Close()
outFile, err := os.Create(outputFile)
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, gzipReader)
return err
}5.2.2 内存管理
在处理大文件时,应注意以下内存管理策略:
- 分块处理:将文件分块读取和写入,避免一次性加载整个文件。
- 使用缓冲区:利用缓冲区来提高 I/O 操作的效率。
- 内存回收:定期进行垃圾回收,释放不再使用的内存。
5.2.3 实践案例
我们可以使用上面的函数将一个大文件 largefile.txt 压缩成 largefile.gz,然后再解压回 decompressed_largefile.txt。
go
func main() {
inputFile := "largefile.txt"
compressedFile := "largefile.gz"
decompressedFile := "decompressed_largefile.txt"
// 压缩大文件
err := compressLargeFile(inputFile, compressedFile)
if err != nil {
log.Fatal(err)
}
fmt.Println("Large file compressed successfully")
// 解压大文件
err = decompressLargeFile(compressedFile, decompressedFile)
if err != nil {
log.Fatal(err)
}
fmt.Println("Large file decompressed successfully")
}运行此代码后,我们会看到控制台输出:
plaintext
Large file compressed successfully
Large file decompressed successfully并且在当前目录下生成 largefile.gz 文件和 decompressed_largefile.txt 文件。