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.Reader、io.Writer。使用这些 interface 的好处是灵活,比如 Decode() 函数的参数并不是一个具体的类型。在下一步,它会从一个 log 文件里输入,而这一步的测试用例,则是从内存里输入。读者可以查看测试用例里的 bytes.Buffer 来学习用法。
其实 Encode() 可以使用 io.Writer,而不是返回 slice。虽然并没有必要这样做。
Unix 下的系统调用(syscall)中的 read、write 是类似的设计。这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 那样广泛流行。很多底层项目都是自创的格式。这是因为二进制序列化太简单了,不值得特意引入一个库。而文本格式由于其复杂性,最好用已有的实现。