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)

要求:

|  长度  | 数据 |
| 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 例:

uint64 和 int64 之间的转换,就是什么都不做。这样的效果是:

有符号整数和无符号整数的区别,只是 CPU 怎么解读这些二进制 bit。再加上它们之间并不存在转换,所以序列化时随便使用编程语言里的类型转换。

符号 bit

有符号整数的最高位表示是否是负数。许多人会想当然的以为,计算机表示正负整数就是正负号+绝对值。然而这种方式只是被一些古代计算机采用,现代计算机都是采用上面这种方式表示负整数,叫做 two’s complement。如果是正负号+绝对值,就应该同时有+0和-0,而±0的概念只在浮点数存在,因为浮点数才是正负号+绝对值这种表示。