如果您压缩一个名为 a.txt 的文件,其中包含文本 "hello"(即 5 个字符),则压缩结果约为 115 个字节。这是否意味着 zip 格式无法有效压缩文本文件?当然不是。有一个开销。如果文件包含"hello" 一百次(500 字节),压缩它会导致文件为120 字节! 1x"hello" => 115 字节,100x"hello" => 120 字节!我们添加了 495 个字节,但压缩后的大小只增加了 5 个字节。
encoding/gob 包也发生了类似的事情:
该实现为流中的每种数据类型编译一个自定义编解码器,当使用单个编码器传输值流时效率最高,分摊编译成本。
当你“第一次”序列化一个类型的值时,该类型的定义也必须被包含/传输,这样解码器才能正确地解释和解码流:
gobs 流是自描述的。流中的每个数据项之前都有其类型的规范,用一小组预定义类型表示。
让我们回到你的例子:
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
e := Entry{"k1", "v1"}
enc.Encode(e)
fmt.Println(buf.Len())
打印出来:
48
现在让我们再编码几个相同的类型:
enc.Encode(e)
fmt.Println(buf.Len())
enc.Encode(e)
fmt.Println(buf.Len())
现在的输出是:
60
72
在Go Playground 上试试。
分析结果:
相同Entry 类型的附加值仅花费12 个字节,而第一个是48 字节,因为还包括类型定义(大约26 个字节),但那是一次性开销。
所以基本上你传输2个strings:"k1"和"v1",它们是4个字节,strings的长度也必须包括在内,使用4字节(int的大小在 32 位架构上)为您提供 12 个字节,这是“最小值”。 (是的,您可以使用较小的类型来表示长度,但这有其局限性。对于较小的数字,可变长度编码将是更好的选择,请参阅 encoding/binary 包。)
总而言之,encoding/gob 可以很好地满足您的需求。不要被最初的印象所迷惑。
如果一个 Entry 的这 12 个字节对您来说太“多”,您可以随时将流包装到 compress/flate 或 compress/gzip 写入器中以进一步减小大小(以换取较慢的编码/解码和进程的内存要求稍高)。
演示:
让我们测试以下 5 个解决方案:
- 使用“裸”输出(无压缩)
- 使用
compress/flate压缩encoding/gob的输出
- 使用
compress/zlib压缩encoding/gob的输出
- 使用
compress/gzip压缩encoding/gob的输出
- 使用
github.com/dsnet/compress/bzip2压缩encoding/gob的输出
我们将编写一千个条目,更改每个条目的键和值,分别为 "k000"、"v000"、"k001"、"v001" 等。这意味着 Entry 的未压缩大小为 4 字节 + 4字节 + 4 字节 + 4 字节 = 16 字节(2x4 字节文本,2x4 字节长度)。
代码如下所示:
for _, name := range []string{"Naked", "flate", "zlib", "gzip", "bzip2"} {
buf := &bytes.Buffer{}
var out io.Writer
switch name {
case "Naked":
out = buf
case "flate":
out, _ = flate.NewWriter(buf, flate.DefaultCompression)
case "zlib":
out, _ = zlib.NewWriterLevel(buf, zlib.DefaultCompression)
case "gzip":
out = gzip.NewWriter(buf)
case "bzip2":
out, _ = bzip2.NewWriter(buf, nil)
}
enc := gob.NewEncoder(out)
e := Entry{}
for i := 0; i < 1000; i++ {
e.Key = fmt.Sprintf("k%3d", i)
e.Val = fmt.Sprintf("v%3d", i)
enc.Encode(e)
}
if c, ok := out.(io.Closer); ok {
c.Close()
}
fmt.Printf("[%5s] Length: %5d, average: %5.2f / Entry\n",
name, buf.Len(), float64(buf.Len())/1000)
}
输出:
[Naked] Length: 16036, average: 16.04 / Entry
[flate] Length: 4120, average: 4.12 / Entry
[ zlib] Length: 4126, average: 4.13 / Entry
[ gzip] Length: 4138, average: 4.14 / Entry
[bzip2] Length: 2042, average: 2.04 / Entry
在Go Playground 上试试。
如您所见:“裸”输出为 16.04 bytes/Entry,略高于计算的大小(由于上面讨论的一次性微小开销)。
当您使用 flate、zlib 或 gzip 压缩输出时,您可以将输出大小减小到大约 4.13 bytes/Entry,大约是理论大小的 26%,我相信这会让您满意。如果没有,您可以使用提供更高效率压缩的库,例如 bzip2,在上面的示例中,2.04 bytes/Entry 是理论大小的 12.7%!
(请注意,对于“真实”数据,压缩率可能会高很多,因为我在测试中使用的键和值非常相似,因此可压缩性非常好;但真实数据的压缩率应该在 50% 左右-寿命数据)。