bit*_*ner 7 serialization struct go gob
我的任务是将C++代码替换为Go,而我是Go API的新手.我使用gob将数百个键/值条目编码到磁盘页面,但gob编码有太多膨胀,不需要.
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type Entry struct {
Key string
Val string
}
func main() {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
e := Entry { "k1", "v1" }
enc.Encode(e)
fmt.Println(buf.Bytes())
}
Run Code Online (Sandbox Code Playgroud)
这会产生很多我不需要的臃肿:
[35 255 129 3 1 1 5 69 110 116 114 121 1 255 130 0 1 2 1 3 75 101 121 1 12 0 1 3 86 97 108 1 12 0 0 0 11 255 130 1 2 107 49 1 2 118 49 0]
Run Code Online (Sandbox Code Playgroud)
我想序列化每个字符串的len,然后是原始字节,如:
[0 0 0 2 107 49 0 0 0 2 118 49]
Run Code Online (Sandbox Code Playgroud)
我正在保存数百万个条目,因此编码中的额外膨胀会使文件大小增加大约x10.
如何在没有手动编码的情况下将其序列化为后者?
icz*_*cza 21
如果您压缩a.txt包含文本"hello"(包含5个字符)的文件,则结果zip将大约为115个字节.这是否意味着zip格式压缩文本文件效率不高?当然不是.有一个开销.如果文件包含"hello"一百次(500字节),压缩它将导致文件为120字节!1x"hello"=> 115字节,100x"hello"=> 120字节!我们添加了495个byes,但压缩后的大小只增加了5个字节.
encoding/gob包裹发生了类似的事情:
该实现为流中的每种数据类型编译自定义编解码器,并且当使用单个编码器传输值流时,最有效,分摊编译成本.
当您"首先"序列化类型的值时,还必须包含/传输该类型的定义,因此解码器可以正确地解释和解码流:
gobs流是自我描述的.流中的每个数据项前面都有一个类型的规范,用一小组预定义类型表示.
让我们回到你的例子:
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
e := Entry{"k1", "v1"}
enc.Encode(e)
fmt.Println(buf.Len())
Run Code Online (Sandbox Code Playgroud)
它打印:
48
Run Code Online (Sandbox Code Playgroud)
现在让我们编码一些相同的类型:
enc.Encode(e)
fmt.Println(buf.Len())
enc.Encode(e)
fmt.Println(buf.Len())
Run Code Online (Sandbox Code Playgroud)
现在的输出是:
60
72
Run Code Online (Sandbox Code Playgroud)
在Go Playground尝试一下.
分析结果:
相同Entry类型的附加值仅花费12个字节,而第一个是48字节,因为还包括类型定义(约26个字节),但这是一次性开销.
所以基本上你发送2个stringS:"k1"和"v1"其是4个字节,长度stringS还必须包括,使用4字节(的大小int在32位体系结构)给你12个字节,这是"最小".(是的,您可以使用较小的类型作为长度,但这将有其局限性.对于小数字,可变长度编码将是更好的选择,请参阅encoding/binary包.)
总而言之,encoding/gob为您的需求做得非常好.不要被最初的印象所迷惑.
如果一个12字节对你Entry来说太"太多",你总是可以将流包装到一个compress/flate或一个compress/gzip写入器中以进一步减小大小(以换取较慢的编码/解码和稍高的进程内存需求).
示范:
让我们测试3个解决方案:
compress/flate压缩的输出encoding/gobcompress/gzip压缩的输出encoding/gob我们将下笔千个条目,改变键和每个值,是"k000","v000","k001","v001"等.这意味着的未压缩尺寸Entry为4字节+ 4字节+ 4字节+ 4字节= 16个字节(2×4字节的文本,2×4字节的长度).
代码如下所示:
names := []string{"Naked", "flate", "gzip"}
for _, name := range names {
buf := &bytes.Buffer{}
var out io.Writer
switch name {
case "Naked":
out = buf
case "flate":
out, _ = flate.NewWriter(buf, flate.DefaultCompression)
case "gzip":
out = gzip.NewWriter(buf)
}
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)
}
Run Code Online (Sandbox Code Playgroud)
输出:
[Naked] Length: 16036, average: 16.04 / Entry
[flate] Length: 4123, average: 4.12 / Entry
[ gzip] Length: 4141, average: 4.14 / Entry
Run Code Online (Sandbox Code Playgroud)
在Go Playground尝试一下.
正如您所看到的那样:"裸"输出16.04 bytes/Entry仅略微超过计算的大小(由于上面讨论的一次性微小开销).
当你使用flate或gzip来压缩输出时,你可以将输出大小减小到大约4.13 bytes/Entry,理论大小约为26%,我相信你满意.(请注意,对于"实际"数据,压缩比可能会高很多,因为我在测试中使用的键和值非常相似,因此非常可压缩;实际数据仍然应该是50%左右).
使用protobuf有效地编码数据.
https://github.com/golang/protobuf
你的主要看起来像这样:
package main
import (
"fmt"
"log"
"github.com/golang/protobuf/proto"
)
func main() {
e := &Entry{
Key: proto.String("k1"),
Val: proto.String("v1"),
}
data, err := proto.Marshal(e)
if err != nil {
log.Fatal("marshaling error: ", err)
}
fmt.Println(data)
}
Run Code Online (Sandbox Code Playgroud)
你创建一个文件,example.proto像这样:
package main;
message Entry {
required string Key = 1;
required string Val = 2;
}
Run Code Online (Sandbox Code Playgroud)
您可以通过运行以下命令从proto文件生成go代码:
$ protoc --go_out=. *.proto
Run Code Online (Sandbox Code Playgroud)
如果愿意,您可以检查生成的文件.
您可以运行并查看结果输出:
$ go run *.go
[10 2 107 49 18 2 118 49]
Run Code Online (Sandbox Code Playgroud)