最直接的评估方式是压缩前后文件大小对比,需调用 close() 后再用 os.stat().size() 计算;测耗时要分 i/o、cpu、flush 三阶段打点;内存占用需关注外部缓冲区而非仅 runtime.readmemstats()。

压缩前后文件大小对比是最直接的评估方式
别依赖“压缩率”这种模糊说法,直接用 os.Stat() 拿原始文件和压缩后文件的 Size() 值做除法。注意:gzip/zstd 等流式压缩器必须调用 Close() 后才能得到最终大小,否则 os.Stat() 读到的是未刷新的临时长度。
常见错误是压缩完立刻 os.Stat(),结果发现压缩文件比源文件还小——其实是尾部校验帧没写入,解压时必然报 zstd: checksum mismatch 或 gzip: invalid checksum。
- 务必在
gzipWriter.Close()或zstd.Encoder.Close()返回nil错误后再查大小 - 对 TB 级文件,避免用
os.ReadFile()加载全量再比对,直接比磁盘文件尺寸即可 - 若走管道(如
io.Pipe),需先关闭写端、再读取全部数据落地为文件,再统计
压缩耗时必须分段测量:I/O + CPU + flush
单纯用 time.Since() 包裹整个压缩流程会掩盖瓶颈。大文件压缩实际由三阶段组成:磁盘读取(I/O bound)、数据压缩(CPU bound)、写入落盘+尾部校验(flush bound)。它们可能被不同资源卡住。
例如启用 zstd.WithEncoderLevel(zstd.SpeedFastest) 后 CPU 时间下降,但若硬盘随机读性能差,整体耗时反而上升——因为分块读取变成了大量 seek。
- 用
time.Now()在bufio.NewReader().Read()前后打点,测纯 I/O 耗时 - 在每次
encoder.Write()前后打点,看单块压缩时间是否稳定(抖动大说明 GC 或内存竞争) - 在
encoder.Close()前后打点,确认 flush 是否拖慢整体(尤其使用os.File作 writer 时)
内存占用不能只看 runtime.ReadMemStats()
runtime.ReadMemStats() 显示的是 Go 运行时堆内存,但大文件压缩的真正压力常在外部缓冲区:比如你用 make([]byte, 8 做读缓冲,或复用 <code>zstd.Encoder 时内部保留的滑动窗口内存。这些不计入 Alloc,却会触发 OS OOM killer。
更准的做法是用系统工具观测进程 RSS:ps -o pid,rss,comm -p $PID 或 cat /proc/$PID/status | grep VmRSS。重点看峰值 RSS 是否随分块大小线性增长——如果是,说明缓冲区没复用或 encoder 实例泄漏。
- 避免每个 goroutine 创建独立
zstd.NewWriter(),改用zstd.Encoder.Reset()复用实例 - 读缓冲区大小建议设为 4–8MB;小于 1MB 会放大 syscall 开销,大于 16MB 容易触发 Linux page cache 淘汰
- 用
pprof的goroutine和heapprofile 验证无 goroutine 泄漏(比如 channel 未消费完就退出)
压缩效果要结合解压验证,不能只信压缩比
高压缩比可能是以牺牲完整性为代价。特别是用 zstd.WithEncoderCRC(false) 关闭校验、或 gzip 设置 gzip.NoCompression 时,压缩快、体积小,但解压失败概率陡增。
真实场景中,必须跑一次完整解压并校验原始内容哈希(如 sha256.Sum256)。不要只比对文件大小或用 gunzip -t 这类浅层检测——它不校验数据块一致性。
- 解压后立即计算
sha256并与原始文件哈希比对,而非仅检查os.IsNotExist - 对分块压缩方案(如 zstd 分块),需额外验证每一块的
Decoder.Header()是否合法 - 若目标是归档(如 tar.gz),解压后还要检查目录结构、文件权限、mtime 是否还原准确
O_DIRECT,encoder.Close() 仍可能触发内核 writeback,导致后续 os.Stat() 或解压操作被阻塞。把 flush 单独拎出来测,比什么都管用。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











