0201: 数据类型
Union
这一步开始考虑用 KV 来实现关系型数据库。跟 Excel 类似,一个数据库里可以容纳多个表,表是由行(row)跟列(column)组成的。每个列可以选择数据类型,而不是像 KV 一样只有 string/bytes。我们会实现两种数据类型:int64 和 []byte。
type CellType uint8
const (
TypeI64 CellType = 1
TypeStr CellType = 2
)
type Cell struct {
Type CellType
I64 int64
Str []byte
}Cell 用来表示不同的数据类型,根据 Type 来决定数据是 I64 还是 Str。 Go 没有 union 类型,所以有些浪费空间。
序列化
这一步实现 Cell 的序列化和反序列化:
func (cell *Cell) Encode(toAppend []byte) []byte {
switch cell.Type {
case TypeI64:
// TODO
case TypeStr:
// TODO
}
}
func (cell *Cell) Decode(data []byte) (rest []byte, err error)要求:
Cell.Encode()把序列化的结果append到输入的 slice。下一步序列化一个 row 时要拼接多个Cell,可以利用同一个 slice,避免多次内存分配。Cell.Decode()从输入的 slice 反序列化,返回 slice 里多余的数据。int64用binary.LittleEndian序列化。[]byte用以下格式:
| 长度 | 数据 |
| 4 字节 | ... |
大小端
CPU 使用固定长度二进制数字,一般支持 8、16、32、64 个二进制位(bit)。第0位是最低的位,比如 0b0101 = 4+1 = 5,从第0位到第3位依次是1、0、1、0。这里你会发现:书写一个数字时,低位(第0位)在右边,高位在左边。而书写一个数组时,第0个元素在左边,高内存地址的元素在右边。这就是大端(big endian)和小端(little endian)的分歧。
一个32位整数在 CPU 寄存器里,只是32个 bit,而储存到内存里,就要分成4个字节。这4个字节如果按照自然顺序(低位在第0个字节,高位在第3个字节),就叫做小端。而按照书写顺序(低位在第3个字节,高位在第0个字节),就叫做大端。比如 0x11223344,用16进制表示:小端是 44 33 22 11,大端是 11 22 33 44。
历史上曾经流行过使用大端的计算机,这就是为什么各种网络协议(TCP/IP)里用大端。然而现在主流是小端。小端的 CPU 读取大端数据,多了个转换的过程。新设计的数据格式、网络协议大都用的小端。然而大端的表示有一些特殊用途之后会遇到。
有符号和无符号整数
如果你动手编码,你可能会发现 binary.LittleEndian 里只有使用 uint64 的方法,而没有 int64 对应的方法。这是因为有符号的整数(signed)和无符号的整数(unsigned)可以直接转换:
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))这里需要了解负数是怎么编码的。以 int64 例:
- 正数 [0, 263) 的编码,与 uint64 一样。
- 负数 [−263, 0) 的编码,与 uint64 里的 [263, 264) 一一对应。
uint64 和 int64 之间的转换,就是什么都不做。这样的效果是:
- 把 uint64 的范围分成两半,int64 里的正数和零对应前一半,负数则对应后一半。
- 对于比较小的正数,uint64 和 int64 表示上没有区别。
有符号整数和无符号整数的区别,只是 CPU 怎么解读这些二进制 bit。再加上它们之间并不存在转换,所以序列化时随便使用编程语言里的类型转换。
符号 bit
有符号整数的最高位表示是否是负数。许多人会想当然的以为,计算机表示正负整数就是正负号+绝对值。然而这种方式只是被一些古代计算机采用,现代计算机都是采用上面这种方式表示负整数,叫做 two’s complement。如果是正负号+绝对值,就应该同时有+0和-0,而±0的概念只在浮点数存在,因为浮点数才是正负号+绝对值这种表示。