首页
学习
活动
专区
工具
TVP
发布

go语言对binary处理的分析总结

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--

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180609G12JLD00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券