golang gzip 使用过程中问题排查
在使用 golang 的 gzip 包压缩数据时发现一个坑,出问题的代码如下:
// 代码1
1 func main() {
2 fmt.Println(compressV1([]byte("test")))
3 }
4
5 func compressV1(data []byte) []byte {
6 var b bytes.Buffer
7 w := gzip.NewWriter(&b)
8 defer func() { _ = w.Close() }()
9
10 _, err := w.Write(data)
11 if err != nil {
12 panic(err)
13 }
14 _ = w.Flush()
15 return b.Bytes()
16 }
输出的结果:
[31 139 8 0 0 0 0 0 0 255 42 73 45 46 1 0 0 0 255 255]
gzip 的 close 函数, 如「代码2」所示,代码会在 15 行的 compressor.Close
中写入 5 位 StoredHeader 信息,在 19 行写入 4 位 crc 信息,20 行写入 4 位数据长度信息。
// 代码2
1 func (z *Writer) Close() error {
2 if z.err != nil {
3 return z.err
4 }
5 if z.closed {
6 return nil
7 }
8 z.closed = true
9 if !z.wroteHeader {
10 z.Write(nil)
11 if z.err != nil {
12 return z.err
13 }
14 }
15 z.err = z.compressor.Close()
16 if z.err != nil {
17 return z.err
18 }
19 le.PutUint32(z.buf[:4], z.digest)
20 le.PutUint32(z.buf[4:8], z.size)
21 _, z.err = z.w.Write(z.buf[:8])
22 return z.err
23 }
我们写 golang 时通常喜欢在 defer
中释放资源,就像是「代码1」中的第 8 行那样,defer 会在 return 之后运行,所以导致 gzip 的 13 位 footer 数据没有写入。稍微修改一下代码,得到「代码3」,在 return 前 close 掉 gzip 的 writer,就可以得到正确的结果了。
// 代码 3
func main() {
fmt.Println(compressV2([]byte("test")))
}
func compressV2(data []byte) []byte {
var b bytes.Buffer
w := gzip.NewWriter(&b)
_, err := w.Write(data)
if err != nil {
panic(err)
}
_ = w.Flush()
_ = w.Close()
return b.Bytes()
}
「代码3」输出如下:
[31 139 8 0 0 0 0 0 0 255 42 73 45 46 1 0 0 0 255 255 1 0 0 255 255 12 126 127 216 4 0 0 0]
如果直接返回 Buffer 会怎么样。
// 代码4
func main() {
b := compressV3([]byte("test"))
fmt.Println(b.Bytes())
}
func compressV3(data []byte) bytes.Buffer {
var b bytes.Buffer
w := gzip.NewWriter(&b)
defer func() { _ = w.Close() }()
_, err := w.Write(data)
if err != nil {
panic(err)
}
_ = w.Flush()
return b
}
依然在 defer
中关闭 gzip 的 writer,返回 bytes.Buffer
。由于返回的是值,所以输出依然是错误的。代码输出如下
[31 139 8 0 0 0 0 0 0 255 42 73 45 46 1 0 0 0 255 255]
继续修改代码。
// 代码5
func main() {
fmt.Println(compressV4([]byte("test")).Bytes())
}
func compressV4(data []byte) *bytes.Buffer {
var b bytes.Buffer
w := gzip.NewWriter(&b)
defer func() { _ = w.Close() }()
_, err := w.Write(data)
if err != nil {
panic(err)
}
_ = w.Flush()
return &b
}
改成返回 bytes.Buffer
的指针,就可以得到正确的结果了。代码输出如下:
[31 139 8 0 0 0 0 0 0 255 42 73 45 46 1 0 0 0 255 255 1 0 0 255 255 12 126 127 216 4 0 0 0]
golang 的 gzip 最坑的是,对于没有写入 footer 的压缩数据可以正常的做解压!!!
总结
如果涉及到关闭之后依然需要写入数据的资源,尽量不要在 defer 中进行 close,要在 return 之前确保资源已关闭。