0102: 二进制序列化

序列化

编程语言中的各种数据类型,要想保存到硬盘里,或者通过网络传输,就得按照某个格式,变成一串字节。这个叫做序列化 (serialization)。例如一对 KV:

type Entry struct {
    key []byte
    val []byte
}

func (ent *Entry) Encode() []byte

按照以下格式实现 Encode() 函数:

| key 大小 | val 大小 | key 数据 | val 数据 |
|  4 bytes |  4 bytes |    ...   |    ...   |

例如 key=a, val=bb,返回 []byte(1, 0, 0, 0, 2, 0, 0, 0, 'a', 'bb')

对于 slice、string 等大小不固定的类型,需要把大小放在前面,才能反序列化。这里大小用 uint32 小端存储。使用 binary.LittleEndian.PutUint32() 将整数转换成4个字节。

func (ent *Entry) Encode() []byte {
    data := make([]byte, 4+4+len(ent.key)+len(ent.val))
    binary.LittleEndian.PutUint32(data[0:4], uint32(len(ent.key)))
    binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.val)))
    copy(data[8:], ent.key)
    copy(data[8+len(ent.key):], ent.val)
    return data
}

反序列化

反序列化就是把一串字节解析回去。接下来实现这个函数:

func (ent *Entry) Decode(r io.Reader) error

调用 Decode() 函数时,调用方不知道要用到多少字节,所以不能传 slice 作为参数。这时就要用到标准库里的 io.Reader 接口了:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Decode() 函数里用 r.Read() 来读取数据,就像读取文件一样。但它是一个 interface,所以背后的实现不一定是文件。

进入 db_project/0102 目录。实现 Entry.Decode() 函数。运行测试用例:

go test .

io.Reader 与 io.Writer

io.Reader 用作输入,对应的 io.Writer 则用作输出。

type Writer interface {
    Write(p []byte) (n int, err error)
}

所以实现了 Read()Write() 方法的类型都可以当做 io.Readerio.Writer。使用这些 interface 的好处是灵活,比如 Decode() 函数的参数并不是一个具体的类型。在下一步,它会从一个 log 文件里输入,而这一步的测试用例,则是从内存里输入。读者可以查看测试用例里的 bytes.Buffer 来学习用法。

其实 Encode() 可以使用 io.Writer,而不是返回 slice。虽然并没有必要这样做。

Unix 下的系统调用(syscall)中的 readwrite 是类似的设计。这2个 syscall 可以操作完全不同类型的资源:文件、网络 socket、pipe、IPC。因为共同点都是可以输入输出。

序列化方法概述

所有的序列化方法都大同小异,只是细节上有区别。要序列化一个长度不固定的数据(字符串),最简单的办法就是把长度放在前面,数据放在后面。长度是一个整数,这个整数怎么编码,可以有区别。有的格式用2个字节,有的用4个字节,有的用长度不固定的 varint,还有像 Redis 那样用十进制数字的。

除了上面这种二进制格式(binary),还有文本格式(text),例如 JSON、XML。这里的 binary 跟“进制”并没有关系,只是作为 text 的反义词。大部分文本格式不编码字符串长度,而是用分隔符(delimiter)来标记数据的终止。JSON 用引号,XML 用 tag。

文本格式虽然看起来直观,实现起来却麻烦。因为编码后的数据不能包含分隔符,文本格式都有复杂的“转义(escape)”。即使是最简单的 JSON,各种库实现里 bug 不少。相对于简单的二进制序列化还浪费 CPU。

除了复杂度,文本格式还有可能有各种使用上的限制,比如 JSON 不能支持任意二进制数据,于是 base64 流行,更加浪费了。很多 JSON 库还不支持 64-bit 整数。所以如果无必要,不要使用文本格式。

二进制序列化方法里,有 Protobuf、MsgPack 等实现,但都没有像 JSON 那样广泛流行。很多底层项目都是自创的格式。这是因为二进制序列化太简单了,不值得特意引入一个库。而文本格式由于其复杂性,最好用已有的实现。