hello world

stay foolish, stay hungry

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 之前确保资源已关闭。