You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Goroutines构建Zip归档时触发切片越界Panic问题排查

问题描述

尝试通过goroutine并行处理文件来优化Zip归档构建函数,目标目录包含190个文件(总计500MB),但运行时触发panic:

panic: runtime error: slice bounds out of range [4126:4096]

错误发生在io.Copy(bundleFile, fsFile)调用处,以下是完整函数代码、错误位置及堆栈跟踪:

函数代码

func BuildArchive() error {                                                         
                                                                                   
    var files []string                                                              
    err := filepath.Walk("/tmp/dir-to-zip", func(filePath string, info os.FileInfo, err error) error {
        if info.IsDir() {                                                          
            return nil                                                             
        }                                                                           
        if err != nil {                                                            
            fmt.Println(err)                                                       
            return err                                                             
        }                                                                           
        files = append(files, filePath)                                            
                                                                                   
        return nil                                                                
    })                                                                             
    if err != nil {                                                                
        return err                                                                
    }                                                                              
                                                                                   
    bundle, err := os.Create("/tmp/archive.zip")                                   
    if err != nil {                                                                
        return err                                                                
    }                                                                              
    bundleWriter := zip.NewWriter(bundle)                                          
                                                                                   
    var wg sync.WaitGroup                                                          
    wg.Add(len(files))                                                             
                                                                                   
    for _, filePath := range files {                                               
        go func(filePath string) {                                                 
            defer wg.Done()                                                        
            relPath := strings.TrimPrefix(filePath, fmt.Sprintf("%v/", filepath.Dir("/tmp/dir-to-zip")))
                                                                                   
            bundleFile, err := bundleWriter.Create(relPath)                        
            if err != nil {                                                        
                fmt.Println(err)                                                   
            }                                                                      
            fsFile, err := os.Open(filePath)                                       
            if err != nil {                                                        
                fmt.Println(err)                                                   
            }                                                                      
            _, err = io.Copy(bundleFile, fsFile)                                   
            if err != nil {                                                        
                fmt.Println(err)                                                   
            }                                                                      
                                                                                   
        }(filePath)                                                                
    }                                                                              
    wg.Wait()                                                                      
                                                                                   
    err = bundleWriter.Close()                                                     
    if err != nil {                                                                
        return err                                                                
    }                                                                              
    return nil                                                                    
} 

错误发生位置

_, err = io.Copy(bundleFile, fsFile)                                   
if err != nil {                                                        
    fmt.Println(err)                                                   
}   

堆栈跟踪

goroutine 48 [running]:                                                            
bufio.(*Writer).Write(0xc00002a100, {0xc00041a400?, 0x3d?, 0xc00041a400?})          
    /usr/local/go/src/bufio/bufio.go:670 +0x1c8                                    
archive/zip.(*countWriter).Write(0xc00000c138, {0xc00041a400?, 0x3d?, 0x4afa20?})
    /usr/local/go/src/archive/zip/writer.go:601 +0x2e                                
io.WriteString({0x4e7538, 0xc00000c138}, {0xc0000212c9, 0x3d})                     
    /usr/local/go/src/io/io.go:314 +0x91                                             
archive/zip.writeHeader({0x4e7538, 0xc00000c138}, 0xc000220090)                      
    /usr/local/go/src/archive/zip/writer.go:422 +0x5ec                               
archive/zip.(*Writer).CreateHeader(0xc0000760a0, 0xc00021e1b0)                       
    /usr/local/go/src/archive/zip/writer.go:378 +0x797                               
archive/zip.(*Writer).Create(0x4e7698?, {0xc0000212c9, 0x3d})                        
    /usr/local/go/src/archive/zip/writer.go:223 +0x6c                                
main.BuildArchive.func2({0xc0000212c0, 0x46})                                        
    /home/simba/go/src/foobar/main.go:79 +0x1c5                                      
created by main.BuildArchive                                                         
    /home/simba/go/src/foobar/main.go:73 +0x5aa                                      
panic: runtime error: slice bounds out of range [:4126] with capacity 4096          
                                                                                   
goroutine 6 [running]:                                                               
bufio.(*Writer).Flush(0xc00002a100)                                                  
    /usr/local/go/src/bufio/bufio.go:634 +0x171                                      
bufio.(*Writer).Write(0xc00002a100, {0xc0001b4200?, 0xc000199b20?, 0xc000199b20?})
    /usr/local/go/src/bufio/bufio.go:672 +0xd8                                       
archive/zip.(*countWriter).Write(0xc00000c138, {0xc0001b4200?, 0x0?, 0xc000199b40?})
    /usr/local/go/src/archive/zip/writer.go:601 +0x2e                                
archive/zip.(*countWriter).Write(0xc000220018, {0xc0001b4200?, 0xc0001b02c0?, 0xc0001b02f0?})
    /usr/local/go/src/archive/zip/writer.go:601 +0x2e                                
compress/flate.(*huffmanBitWriter).write(...)                                        
    /usr/local/go/src/compress/flate/huffman_bit_writer.go:136                       
compress/flate.(*huffmanBitWriter).writeCode(0xc0001b41e0?, {0x6000?, 0x22?})    
    /usr/local/go/src/compress/flate/huffman_bit_writer.go:347 +0xe5                
compress/flate.(*huffmanBitWriter).writeTokens(0xc0001b41e0, {0xc002558000, 0x4001, 0x403f800000403f?}, {0xc0001aa900, 0x11e, 0x108129000000000f?}, {0xc0001ac100, 0x1e, 0x1e})
    /usr/local/go/src/compress/flate/huffman_bit_writer.go:583 +0xb9                
compress/flate.(*huffmanBitWriter).writeBlock(0xc0001b41e0, {0xc002558000?, 0x20?, 0xd79?}, 0x0, {0x0, 0x0, 0x0})
    /usr/local/go/src/compress/flate/huffman_bit_writer.go:495 +0x490               
compress/flate.(*compressor).writeBlock(0xc0005a2000, {0xc002558000?, 0xc000032f00?, 0xc000199d28?}, 0x47739b?)
    /usr/local/go/src/compress/flate/deflate.go:170 +0x9c                            
compress/flate.(*compressor).deflate(0xc0005a2000)                                   
    /usr/local/go/src/compress/flate/deflate.go:509 +0x59b                           
compress/flate.(*compressor).write(0xc0005a2000, {0xc00256a000?, 0x8000, 0xf311b6fd?})
    /usr/local/go/src/compress/flate/deflate.go:554 +0x82                            
compress/flate.(*Writer).Write(...)                                                  
    /usr/local/go/src/compress/flate/deflate.go:712                                  
archive/zip.(*pooledFlateWriter).Write(0xc00020c040?, {0xc00256a000?, 0x8000?, 0x4af140?})
    /usr/local/go/src/archive/zip/register.go:51 +0xc5                               
archive/zip.(*countWriter).Write(...)                                                
    /usr/local/go/src/archive/zip/writer.go:601                                      
archive/zip.(*fileWriter).Write(0xc000222000, {0xc00256a000, 0x8000, 0x8000})    
    /usr/local/go/src/archive/zip/writer.go:533 +0x97                                
io.copyBuffer({0x4e7558, 0xc000222000}, {0x4e7678, 0xc0001f8008}, {0x0, 0x0, 0x0})
    /usr/local/go/src/io/io.go:428 +0x204                                            
io.Copy(...)                                                                       
    /usr/local/go/src/io/io.go:385                                                 
main.BuildArchive.func2({0xc00001c0c0, 0x35})                                        
    /home/simba/go/src/foobar/main.go:89 +0x385                                      
created by main.BuildArchive                                                         
    /home/simba/go/src/foobar/main.go:73 +0x5aa                                      
exit status 2

问题原因与修复方案

核心原因

archive/zip.Writer不是并发安全的,它内部维护了共享的缓冲区和状态信息(比如全局的bufio.Writer),当多个goroutine同时调用Create方法或写入文件内容时,会导致并发写冲突,破坏内部数据结构,最终触发slice越界的panic。

从堆栈跟踪也能看出,panic发生在bufio.Writer的Flush和Write操作中,这是多个goroutine同时操作同一个缓冲区导致的数据竞争。

修复方案

方案1:对zip.Writer的操作加互斥锁

在goroutine中,所有涉及bundleWriter的操作(Create、写入文件内容)都需要加锁,确保同一时间只有一个goroutine操作zip.Writer:

修改后的代码:

func BuildArchive() error {                                                         
    var files []string                                                              
    err := filepath.Walk("/tmp/dir-to-zip", func(filePath string, info os.FileInfo, err error) error {
        if info.IsDir() {                                                          
            return nil                                                             
        }                                                                           
        if err != nil {                                                            
            fmt.Println(err)                                                       
            return err                                                             
        }                                                                           
        files = append(files, filePath)                                            
        return nil                                                                
    })                                                                             
    if err != nil {                                                                
        return err                                                                
    }                                                                              
                                                                                   
    bundle, err := os.Create("/tmp/archive.zip")                                   
    if err != nil {                                                                
        return err                                                                
    }                                                                              
    defer bundle.Close() // 增加defer关闭文件,避免句柄泄漏
                                                                                   
    bundleWriter := zip.NewWriter(bundle)                                          
    defer bundleWriter.Close() // 用defer简化关闭逻辑
                                                                                   
    var wg sync.WaitGroup                                                          
    var mu sync.Mutex // 新增互斥锁,保护zip.Writer的并发操作
                                                                                   
    wg.Add(len(files))                                                             
                                                                                   
    for _, filePath := range files {                                               
        go func(filePath string) {                                                 
            defer wg.Done()                                                        
            relPath := strings.TrimPrefix(filePath, fmt.Sprintf("%v/", filepath.Dir("/tmp/dir-to-zip")))
            
            // 打开文件放在锁外,减少锁占用时间
            fsFile, err := os.Open(filePath)                                       
            if err != nil {                                                        
                fmt.Println(err)                                                   
                return
            }                                                                      
            defer fsFile.Close() // 新增defer关闭文件
            
            mu.Lock() // 加锁保护zip.Writer的操作
            defer mu.Unlock()
            
            bundleFile, err := bundleWriter.Create(relPath)                        
            if err != nil {                                                        
                fmt.Println(err)                                                   
                return
            }                                                                      
            _, err = io.Copy(bundleFile, fsFile)                                   
            if err != nil {                                                        
                fmt.Println(err)                                                   
            }                                                                      
        }(filePath)                                                                
    }                                                                              
    wg.Wait()                                                                      
                                                                                   
    return nil                                                                    
} 

方案2:并行读取文件,串行写入Zip

如果想最大化并行效率,可以先在goroutine中读取文件内容到内存,然后串行写入Zip。这种方式适合小文件场景,大文件会占用过多内存,需要根据实际情况权衡:

func BuildArchive() error {
    // 前面的文件遍历逻辑不变
    var files []string                                                              
    err := filepath.Walk("/tmp/dir-to-zip", func(filePath string, info os.FileInfo, err error) error {
        if info.IsDir() {                                                          
            return nil                                                             
        }                                                                           
        if err != nil {                                                            
            fmt.Println(err)                                                       
            return err                                                             
        }                                                                           
        files = append(files, filePath)                                            
        return nil                                                                
    })                                                                             
    if err != nil {                                                                
        return err                                                                
    }                                                                              

    bundle, err := os.Create("/tmp/archive.zip")
    if err != nil {
        return err
    }
    defer bundle.Close()

    bundleWriter := zip.NewWriter(bundle)
    defer bundleWriter.Close()

    // 定义结构体存储文件路径、内容和读取错误
    type fileData struct {
        relPath string
        content []byte
        err     error
    }

    ch := make(chan fileData, len(files))
    var wg sync.WaitGroup

    // 并行读取文件
    wg.Add(len(files))
    for _, filePath := range files {
        go func(filePath string) {
            defer wg.Done()
            relPath := strings.TrimPrefix(filePath, fmt.Sprintf("%v/", filepath.Dir("/tmp/dir-to-zip")))
            
            content, err := os.ReadFile(filePath)
            ch <- fileData{relPath: relPath, content: content, err: err}
        }(filePath)
    }

    // 所有文件读取完成后关闭通道
    go func() {
        wg.Wait()
        close(ch)
    }()

    // 串行写入Zip归档
    for fd := range ch {
        if fd.err != nil {
            fmt.Println(fd.err)
            continue
        }
        bundleFile, err := bundleWriter.Create(fd.relPath)
        if err != nil {
            fmt.Println(err)
            continue
        }
        _, err = bundleFile.Write(fd.content)
        if err != nil {
            fmt.Println(err)
        }
    }

    return nil
}

额外注意事项

  • 原代码未关闭fsFile,会导致文件句柄泄漏,修复时需添加defer fsFile.Close()。
  • 并行处理Zip归档的性能提升有限,因为磁盘IO是串行瓶颈;若开启压缩,CPU也可能成为瓶颈,需根据实际场景选择是否并行。

内容的提问来源于stack exchange,提问作者SimbaPK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 03:35:18