必须手动压缩每份原始[]byte并取真实字节长度计算压缩比,禁用os.Stat()读.gz文件大小反推,因gzip头尾开销、未知原始长度及额外字段会导致失真;流式分治处理防OOM,CSV导出需转义与精度控制。

直接批量导出压缩比数据,核心就一条:别碰文件系统统计值,必须对每份原始 []byte 手动压缩、取真实字节长度、算比值。任何基于 os.Stat().Size() 的做法都会因 gzip header/trailer、文件系统块对齐、缓存未刷盘等问题失真。
为什么不能用 os.Stat() 读取 .gz 文件大小来反推压缩比
gzip 文件自带 10 字节 header + 8 字节 trailer(CRC32),而原始数据长度你根本不知道——除非你当初就存了它。比如一个 1000 字节的 JSON,压缩后写入 data.json.gz,os.Stat("data.json.gz").Size() 返回的是 1023,但你无法知道那 23 是 header/trailer 还是压缩膨胀。更糟的是,如果这个 .gz 文件是别人生成的(比如 nginx 自动 gzip),你还得确认它是否带 Name 或 Comment 字段(多出 10~30 字节)。所以:绝对不要从已有 .gz 文件倒推原始长度。
- 错误做法:
ratio = float64(origStat.Size()) / float64(gzStat.Size()) - 正确前提:你必须持有原始
[]byte,且全程自己控制压缩流程 - 例外场景:只做相对比较(如 A 文件比 B 文件压缩得更好),且所有文件都用同一套代码、同一参数生成,此时可跳过原始数据重载,但依然要统一用
buf.Len()取压缩后长度
批量计算时如何避免内存爆炸
全量加载所有文件到内存再压缩,对几百 MB 的日志目录就是 OOM。关键是分治:单文件流式读取 → 小块缓冲 → 压缩器复用 → 记录长度。不需要把整个文件塞进 []byte。
- 对每个文件,用
os.Open()获取*os.File,然后传给io.Copy(gw, src),gw是复用的*gzip.Writer - 压缩完成后调用
gw.Close(),再用buf.Len()拿压缩后字节数;原始长度用file.Stat().Size()—— 此时它是可信的,因为文件没被修改过,且你没做任何预处理(如 trim) - 每次处理完清空
buf.Reset(),而不是新建bytes.Buffer,减少 GC 压力 - 若需校验一致性(比如怕流式读损坏),加一层
hash.Hash在io.TeeReader(file, hash)上累加原始哈希,但哈希本身不参与压缩比计算
导出 CSV 时要注意字段转义和浮点精度
压缩比结果是 float64,直接 fmt.Fprint 到 CSV 容易出现科学计数法(如 1.23456789e+01)或过多小数位,下游 Excel 打开会错乱。必须格式化为固定小数位字符串,并包裹双引号。
立即学习“go语言免费学习笔记(深入)”;
- 用
fmt.Sprintf("%.3f", ratio)控制精度,3 位足够(压缩比极少超过 100×) - 文件路径含逗号、换行、双引号时,必须用双引号包裹,且内部双引号要转义为两个双引号(
"→"") - 别手写 CSV:用
encoding/csv包,它自动处理转义:w.Write([]string{path, strconv.FormatFloat(ratio, 'f', 3, 64)}) - 第一行写 header:
[]string{"file_path", "original_size", "compressed_size", "ratio"}
最易被忽略的一点:gzip 和 zlib 的压缩比不能混在同一张表里直接对比。哪怕内容完全一样,gzip 输出必然比 zlib 多约 10 字节,这对小文件(algorithm("gzip" 或 "zlib"),否则后续分析会误判。


















