I've been tasked to replace C++ code to Go and I'm quite new to the Go APIs. I am using gob for encoding hundreds of key/value entries to disk pages but the gob encoding has too much bloat that's not needed.
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())
}
This produces a lot of bloat that I don't need:
[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]
I want to serialize each string's len followed by the raw bytes like:
[0 0 0 2 107 49 0 0 0 2 118 49]
I am saving millions of entries so the additional bloat in the encoding increases the file size by roughly x10.
How can I serialize it to the latter without manual coding?
"Manual coding", you're so afraid of, is trivially done in Go using the standard
encoding/binary
package.You appear to store string length values as 32-bit integers in big-endian format, so you can just go on and do just that in Go:
Playground link.
Note that in this example I'm writing to a byte buffer, but that's for demonstration purposes only—since
encode()
writes to anio.Writer
, you can pass it an opened file, a network socket and anything else implementing that interface.Use protobuf to efficiently encode your data.
https://github.com/golang/protobuf
Your main would look like this:
You create a file, example.proto like this:
You generate the go code from the proto file by running:
You can examine the generated file, if you wish.
You can run and see the results output:
If you zip a file named
a.txt
containing the text"hello"
(which is 5 characters), the result zip will be around 115 bytes. Does this mean the zip format is not efficient to compress text files? Certainly not. There is an overhead. If the file contains"hello"
a hundred times (500 bytes), zipping it will result in a file being 120 bytes!1x"hello"
=> 115 bytes,100x"hello"
=> 120 bytes! We added 495 byes, and yet the compressed size only increased by 5 bytes.Something similar is happening with the
encoding/gob
package:When you "first" serialize a value of a type, the definition of the type also has to be included / transmitted, so the decoder can properly interpret and decode the stream:
Let's return to your example:
It prints:
Now let's encode a few more of the same type:
Now the output is:
Try it on the Go Playground.
Analyzing the results:
Additional values of the same
Entry
type only cost 12 bytes, while the first is48
bytes because the type definition is also included (which is ~26 bytes), but that is a one-time overhead.So basically you transmit 2
string
s:"k1"
and"v1"
which are 4 bytes, and the length ofstring
s also has to be included, using4
bytes (size ofint
on 32-bit architectures) gives you the 12 bytes, which is the "minimum". (Yes, you could use a smaller type for length, but that would have its limitations. A variable-length encoding would be a better choice for small numbers, seeencoding/binary
package.)All in all,
encoding/gob
does a pretty good job for your needs. Don't get fooled by initial impressions.If this 12 bytes for one
Entry
is too "much" for you, you can always wrap the stream into acompress/flate
orcompress/gzip
writer to further reduce the size (in exchange for slower encoding/decoding and slightly higher memory requirement for the process).Demonstration:
Let's test the 3 solutions:
compress/flate
to compress the output ofencoding/gob
compress/gzip
to compress the output ofencoding/gob
We will write a thousand entries, changing keys and values of each, being
"k000"
,"v000"
,"k001"
,"v001"
etc. This means the uncompressed size of anEntry
is 4 byte + 4 byte + 4 byte + 4 byte = 16 bytes (2x 4 bytes text, 2x4 byte lengths).The code looks like this:
Output:
Try it on the Go Playground.
As you can see: the "naked" output is
16.04 bytes/Entry
, just little over the calculated size (due to the one-time tiny overhead discussed above).When you use flate or gzip to compress the output, you can reduce the output size to about
4.13 bytes/Entry
, which is about ~26% of the theoretical size, I'm sure that satisfies you. (Note that with "real-life" data the compression ratio would probably be a lot higher as the keys and values I used in the test are very similar and thus really well compressible; still ratio should be around 50% with real-life data).