我的任务是将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())
}
这会产生很多我不需要的臃肿:p>
[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]
我想序列化每个字符串的len,然后是原始字节,如:
[0 0 0 2 107 49 0 0 0 2 118 49]
我正在保存数百万个条目,因此编码中的额外膨胀会使文件大小增加大约x10。
如何在没有手动编码的情况下将其序列化为后者?
答案 0 :(得分:20)
如果您压缩包含文本a.txt
(5个字符)的名为"hello"
的文件,则结果zip将大约为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 string
s:"k1"
和"v1"
这些是4个字节,并且string
s的长度也必须包括在内,使用{{ 1}}字节(32位体系结构上4
的大小)为您提供12个字节,即"最小值"。 (是的,您可以使用较小的类型作为长度,但这有其局限性。对于小数字,可变长度编码将是更好的选择,请参阅encoding/binary
包。)
总而言之,int
可以很好地满足您的需求。不要被最初的印象所迷惑。
如果一个encoding/gob
的这12个字节太多"很多"对于您来说,您始终可以将流包装到compress/flate
或compress/gzip
编写器中以进一步缩小大小(以换取较慢的编码/解码以及对该过程稍高的内存要求)。
<强>演示:强>
让我们测试3个解决方案:
Entry
压缩compress/flate
encoding/gob
压缩compress/gzip
我们将编写一千个条目,更改每个条目的键和值,为encoding/gob
,"k000"
,"v000"
,"k001"
等。这意味着未压缩的大小"v001"
是4字节+4字节+4字节+ 4字节= 16字节(2x 4字节文本,2x4字节长度)。
代码如下所示:
Entry
输出:
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)
}
在Go Playground上尝试。
正如你所看到的那样:&#34;裸体&#34;输出为[Naked] Length: 16036, average: 16.04 / Entry
[flate] Length: 4123, average: 4.12 / Entry
[ gzip] Length: 4141, average: 4.14 / Entry
,仅略高于计算的大小(由于上面讨论过的一次性微小开销)。
使用flate或gzip压缩输出时,可以将输出大小减小到约16.04 bytes/Entry
,约为理论大小的约26%,我确信满足您的要求。 (请注意,使用&#34;现实生活&#34;数据压缩率可能要高很多,因为我在测试中使用的键和值非常相似,因此非常可压缩;仍然比率应该在50%左右现实生活中的数据)。
答案 1 :(得分:9)
使用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)
}
您可以像这样创建一个example.proto文件:
package main;
message Entry {
required string Key = 1;
required string Val = 2;
}
通过运行:
从proto文件生成go代码$ protoc --go_out=. *.proto
如果愿意,您可以检查生成的文件。
您可以运行并查看结果输出:
$ go run *.go
[10 2 107 49 18 2 118 49]
答案 2 :(得分:3)
&#34;手动编码&#34;,您非常害怕,使用标准encoding/binary
package在Go中轻松完成。
您似乎将字符串长度值以big-endian格式存储为32位整数,因此您可以继续在Go中执行此操作:
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
func encode(w io.Writer, s string) (n int, err error) {
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(s)))
n, err = w.Write(hdr[:])
if err != nil {
return
}
n2, err := io.WriteString(w, s)
n += n2
return
}
func main() {
var buf bytes.Buffer
for _, s := range []string{
"ab",
"cd",
"de",
} {
_, err := encode(&buf, s)
if err != nil {
panic(err)
}
}
fmt.Printf("%v\n", buf.Bytes())
}
请注意,在此示例中,我写入字节缓冲区,但仅用于演示目的 - 因为encode()
写入io.Writer
,您可以将其传递给打开文件,网络套接字以及实现该接口的任何其他内容。