需封装 CountingWriter 实时统计:original 累加 Write 输入长度,written 累加底层 Write 实际返回字节数,压缩比 = original / written,Close 后计算,注意空输入和部分写入边界。

怎么用 gzip.Writer 实时拿到压缩比
Go 标准库的 gzip.Writer 本身不提供「当前压缩比」接口,它只负责写入并压缩数据流,Write 方法返回的是原始字节数(即未压缩数据长度),而压缩后的真实字节数需手动跟踪底层 io.Writer 的写入量。
关键点在于:压缩比 = 原始字节数 / 压缩后字节数,二者必须在同一流程中同步统计,不能靠事后读取文件大小——那会破坏流式特性。
- 不要用
os.Stat查临时文件大小,这违背流式初衷,且无法用于网络响应等无文件场景 - 不要在
gzip.Writer.Close()后才统计,因为部分数据可能还滞留在内部缓冲区未写出 - 推荐把
gzip.Writer包裹在一个自定义的io.Writer中,在每次Write调用前后分别累加原始和压缩字节数
如何构造带统计能力的流式 gzip 写入器
最轻量的做法是封装一层 io.Writer,把底层写入目标(比如 bytes.Buffer 或 net.Conn)和计数逻辑绑在一起。注意:必须在调用 gzip.Writer.Write 之后、再读取底层 Writer 的实际写入量,否则会漏掉 gzip 内部 flush 的尾部数据。
示例核心逻辑:
立即学习“go语言免费学习笔记(深入)”;
// 统计器结构体
type CountingWriter struct {
w io.Writer
written int64 // 压缩后真实写入字节数
original int64 // 原始输入字节数
}
func (c *CountingWriter) Write(p []byte) (n int, err error) {
n, err = c.w.Write(p)
c.written += int64(n)
c.original += int64(len(p))
return
}
// 使用时:
buf := &bytes.Buffer{}
cw := &CountingWriter{w: buf}
gz := gzip.NewWriter(cw)
gz.Write([]byte("hello world...")) // 原始数据
gz.Close() // 必须 close,否则尾部 header/trailer 不写出,c.written 少计
ratio := float64(cw.original) / float64(cw.written)
为什么 gzip.Writer 的 Write 返回值不能直接当压缩后长度
gzip.Writer.Write 返回的是「接受的原始字节数」,不是压缩后写入的字节数。这是 Go 文档明确规定的:Write(p []byte) (n int, err error) 中的 n 等于 len(p)(除非出错)。它完全不反映压缩效果。
- 压缩是异步缓冲的:小块数据可能被暂存,直到缓冲区满或
Close才真正写出 - gzip 头部(10 字节)、校验和(8 字节)、ISIZE 字段(4 字节)只在
Close时追加,这些都不计入任何一次Write的返回值 - 如果你只看
Write返回值累计,会严重低估压缩后体积,尤其对短流或分多次小写入的场景
流式压缩比计算容易被忽略的边界点
真实服务中,压缩比波动大,但几个点常被跳过验证:
-
gzip.NewWriterLevel传gzip.NoCompression时,压缩后体积 ≈ 原始 + 18 字节(header + trailer),此时比值接近 1.0,但不是 1.0 —— 必须实测,不能假设 - 空输入(
Write(nil)或Write([]byte{}))会导致gzip.Writer仍写出完整头部+trailer(18 字节),此时压缩比为 0/18 → 0,需单独处理避免除零 - 如果底层
io.Writer是网络连接,Write可能返回部分写入(n < len(p)),这时CountingWriter的written累加必须严格等于实际返回的n,不能硬塞len(p)
流式压缩比不是一锤定音的数字,它随输入节奏、缓冲策略、压缩等级动态变化;想稳定获取,唯一可靠路径是控制写入链路,在紧贴底层 Write 的位置做原子计数。


















