解读 Golang 标准库里的 varint 实现
最近发现 Golang 标准库竟然自带了 varint 的实现,代码位置在 encoding/binary/varint.go,这个跟protobuf里面的varint实现基本是一致的。刚好借助 golang 标准库的 varint 源码,我们来系统地学习和梳理下 varint。
熟悉 protobuf 的人肯定对 varint 不陌生,protobuf 里面除了带 fix (如 fixed32、fixed64) 之外的整数类型, 都是 varint 编码。
varint 的出现主要是为了解决两个问题:
- 空间效率:以 uint64 类型为例,可以表示的最大值为 18446744073709551615。然而在实际业务场景中,我们通常处理的整数值远小于 uint64 的最大值。假设在我们的业务中,需要处理的整数值仅为 1,但在网络传输过程中,我们却需要使用 8 个字节来表示这个值。这就导致了大量的空间浪费,因为大部分字节并没有实际存储有效的信息。varint 编码通过使用可变长度的字节序列来表示整数,使得小的整数可以用更少的字节表示,提高空间效率。
- 兼容性:varint 使得我们可以在不改变编码 / 解码逻辑的情况下,处理不同大小的整数。这意味着我们可以在不破坏向后兼容性的情况下,将一个字段从较小的整数类型(如 uint32)升级到较大的整数类型(如 uint64)
本文将通过分析 Golang 标准库自带的 varint 源码实现,介绍 varint 的设计原理以及Golang标准库是如何解决 varint 在编码负数时遇到的问题。
varint 的设计原理
varint 的设计原理非常简单:
- 7bit 为一组:将整数的二进制表示分为 7 个 bit 位一组。从低位到高位,每 7 个 bit 位作为一个单元。
- 最高位表示 “继续” 标志。在每个 7 位的单元前面添加一个标志位,形成一个 8 位的字节。如果后面还有更多的字节,这个标志位就设置为 1,否则设置为 0。
例如:对于一个整数 300,它的二进制表示是 100101100。我们可以将它分为两组,即 10 和 0101100。然后在每组前面添加标志位,得到两个字节 00000010 和 10101100,这两个字节就是 300 的 varint 编码。相比于用 uint32 的 4 字节表示,少了 50% 的存储空间。
无符号整数的 varint
在 Golang 标准库中有两套 varint 的函数: 分别用于无符号整数的 PutUvarint 和 Uvarint,以及用于有符号整数的 Varint 和 PutVarint。
我们先看下无符号整数的 varint 实现,代码如下:
1 | func PutUvarint(buf []byte, x uint64) int { |
代码里有个非常重要的常量:0x80,对应于二进制编码就是 1000 0000。这个常量对接下来的逻辑非常重要:
x >= 0x80。这意味着 x 的二进制表示形式至少有 8 位,我们刚才讲到 7 个 bit 位为一组,那 x 就需要被拆分了。byte(x) | 0x80。将 x 的最低...
剩余内容已隐藏