go语言的encoding/binary包实现了对binary的处理,本文主要是对该包做简要的分析和总结。
text vs binary
在介绍binary包之前,首先简要对比一下text和binary两种编码方式的优缺点。众所周知,在网络通信的时候,可以采用text或者binary两种方式传输数据。在传输效率上,binary明显优于text。比如发送一个数字“18”,采用binary,只需要发送一个字节0x12;而如果采用text,则要发送两个字节:0x31,0x38(分别是字符1和8的ASCII码)。但text的优点是可读性强,比如JSON。所以如果对性能要求很高,就采用binary;否则就考虑text。本文重点是简要分析总结go语言中对binary的处理。
字节序(endianess)
处理binary数据,必然涉及到字节序的问题。有过C/C++开发经验的工程师对字节序这个概念应该都比较熟悉。字节序主要是指存储数值时在内存中对字节的存储顺序。Wikipedia对endianess的定义如下:
Endianness refers to the sequential order in which bytes are arranged into larger numerical values when stored in memory or when transmitted over digital links
如果以前没有接触过字节序这个概念的人,估计看了上面的描述还是一头雾水。所以这里举一个比较典型的例子。假设有一个整数,用16进制表示为0x11223344,在内存中占4个字节。如果在内存中如下存储,则称为大端序(Big-Endian)。最重要的数字存储在最低地址(最左边)处。most significant byte first。
0x11 0x22 0x33 0x44
下面则是小端序(Little-Endian)的存储方式。最不重要的数字存储在最低地址处。least significant byte first。
0x44 0x33 0x22 0x11
在网络上传输一般采用大端序,所以大端序也称为网络序。
encoding/binary
go语言对binary的处理实现在encoding/binary包中。详细的文档如下:
https://golang.org/pkg/encoding/binary/
本文不会逐个介绍encoding/binary包中每一个函数或接口,因为官方文档已经很详细了。这里主要是梳理并对该包中的所有接口进行分类。
encoding/binary中的API可以分成两大类:
1、对定长数值处理的API
2、对变长数值处理的API
上面每一种还可以继续分为两小类:
1、对内存slice处理的API
注:go语言中,slice是对数组的封装。
2、对流数据处理的API
所以总的来说encoding/binary中的API可以分为四类。
1、对内存slice中定长数值的处理
这种类型的接口定义如下:
type ByteOrder interface {
Uint16([]byte) uint16
Uint32([]byte) uint32
Uint64([]byte) uint64
PutUint16([]byte, uint16)
PutUint32([]byte, uint32)
PutUint64([]byte, uint64)
String() string
}
在encoding/binary包中对这个接口有两个实现,分别是LittleEndian和BigEndian。定义如下:
// LittleEndian is the little-endian implementation of ByteOrder.
var LittleEndian littleEndian
// BigEndian is the big-endian implementation of ByteOrder.
var BigEndian bigEndian
2、对流中定长数值的处理
定义如下:
func Read(r io.Reader, order ByteOrder, data interface{}) error
func Write(w io.Writer, order ByteOrder, data interface{}) error
注意,上面函数Read的最后一个参数data必须是指向一个固定长度数值的指针或者是一个slice。
3、对内存slice中变长数值的处理
定义如下:
func PutUvarint(buf []byte, x uint64) int
func PutVarint(buf []byte, x int64) int
func Uvarint(buf []byte) (uint64, int)
func Varint(buf []byte) (int64, int)
前两个函数用于存储,后两个函数用于读取。可能有人会有疑问,参数的数据类型是uint64/int64,都是8个字节,不都是定长的吗?这里说的变长是指实际占用的长度不确定。虽然uint64/int64最长是8个字节,但实际上很多数据占用的长度是小于8个字节的。上面的PutUvariant/PutVariant返回的整数就是实际写入的长度。
4、对流中变长数值的处理
定义如下:
func ReadUvarint(r io.ByteReader) (uint64, error)
func ReadVarint(r io.ByteReader) (int64, error)
看到这里,估计很多人会问,为什么没有对应的WriteUvaiant/WriteVariant?其实我也有这个疑问。这个问题我还特意到go语言的社区去问了,得到的回复是很容易将内存slice中的数据写入到流中。意思就是开发者自己很容易实现,所以在go的标准库中就没必要实现了。到底有多简单呢?那我就试着自己实现了WriteUvariant,如下:
func WriteUvarint(w io.ByteWriter, x uint64) int {
i := 0
for x >= 0x80 {
w.WriteByte(byte(x) | 0x80)
i++
}
w.WriteByte(byte(x))
return i + 1
}
分析过PutUvarint源码的人很快就会发现,上面这个实现和PutUvarint很像。没错,我就是照着PutUvarint实现的WriteUvarint。既然认为WriteUvarint过于简单,所以没必要实现,那么为什么要实现PutUvarint?费解!我在go语言几年前的一个code review中,的确看到了WriteUvarint/WriteVarint的身影,不知道后来为什么没有merge到master分支中去。当然了,这也不是什么大事,只是感觉go语言标准库的开发团队有点过于任性。套用一句俗语,有钱就是任性,而go语言的团队则是“牛B就是任性”。
变长数据处理
这里对变长数据处理的方式简要描述一下,因为我相信很多人看到我上面实现的WriteUvarint一定有疑问。既然是变长,那么自然就需要有判断数据长度的方法。其实很简单, 就是使用每个字节的第一个bit,称为msb。当msb为1时,表示后面还有更多的数据,否则表示后面没有数据了。也就是说每个byte中,只有7个bits用来存储数据。
注:第一个bit,用专业术语说就是most significant bit,简称msb
现举例说明。例如0x0102,实际需要2个字节(更准确的说是9个bit)。用二进制表示0x0102就是:
1 0000 0010
变长编码之后,就如下:
1000 0010000 0010
第一个字节的msb为1,表示后面还有更多的数据,第二个字节的msb为0,表示后面没有数据了。
注意这里采用的是类似小端序的存储方式,也就是最不重要的字节(准确来说不是1个字节,因为只有7个bit)存储在最低地址处(最左边),用专业术语说就是least significant group first。细心的人可能已经观察到对于定长的接口函数都有一个参数ByteOrder,而变长的接口则没有。
应用场景
根据官方文档的描述,encoding/binary的实现偏重于简洁而不是效率。所以如果需要高性能的序列化,则应该考虑采用encoding/gob包,或者protocol buffer。Protocol Buffer是一个语言中立、平台中立的结构化数据序列化的通用解决方案,具体参考如下链接:
https://github.com/google/protobuf
--END--
领取专属 10元无门槛券
私享最新 技术干货