golang IO包的妙用

v2-3a73a6ed0d6c243d734ba4cd779cf662_b

golang标准库对io的抽象非常精巧,各个组件可以随意组合,可以作为接口设计的典范。这篇文章结合一个实际的例子来和大家分享一下。

背景

以一个RPC的协议包来说,每个包有如下结构

其中TotalSize是整个包除去TotalSize后的字节数, Magic是一个固定长度的字串,Payload是包的实际内容,包含业务逻辑的数据。
Checksum是对MagicPayloadadler32校验和。

编码(encode)

我们使用一个原型为func EncodePacket(w io.Writer, payload []byte) error的函数来把数据打包,结合encoding/binary我们很容易写出第一版,演示需要,错误处理方面就简化处理了。

在上面的实现中,为了计算checksum,我们使用了一个内存buffer来缓存数据,最后把所有的数据一次性读出来算checksum,考虑到计算checksum是一个不断update地过程,我们应该有方法直接略过内存buffer而计算checksum。

查看hash/adler32我们得知,我们可以构造一个Hash32的对象,这个对象内嵌了一个Hash的接口,这个接口的定义如下:

这是一个通用的计算hash的接口,标准库里面所有计算hash的对象都实现了这个接口,比如md5, crc32等。由于Hash实现了io.Writer接口,因此我们可以把所有要计算的数据像写入文件一样写入到这个对象中,最后调用Sum(nil)就可以得到最终的hash的byte数组。利用这个思路,第二版可以这样写:

注意这次的变化,前面写入TotalSize,Magic,Payload部分没有变化,在计算checksum的时候去掉了bytes.Buffer,减少了一次内存申请和拷贝。

考虑到sumw都是io.Writer,利用神奇的io.MultiWriter,我们可以这样写

注意MultiWriter的使用,我们把wsum利用MultiWriter绑在了一起创建了一个新的Writer,向这个Writer里面写入数据就同时向wsum里面都写入数据,这样就完成了发送数据和计算checksum的同步进行,而对于binary.Write来说没有任何区别,因为它需要的是一个实现了Write方法的对象。

解码(decode)

基于上面的思想,解码也可以把接收数据和计算checksum一起进行,完整代码如下

上面代码中,我们使用了io.TeeReader,这个函数的原型为func TeeReader(r Reader, w Writer) Reader,它返回一个Reader,这个Reader是参数r的代理,读取的数据还是来自r,不过同时把读取的数据写入到w里面。

一切皆文件

unix下有一切皆文件的思想,golang把这个思想贯彻到更远,因为本质上我们对文件的抽象就是一个可读可写的一个对象,也就是实现了io.Writerio.Reader的对象我们都可以称为文件,在上面的例子中无论是EncodePacket还是DecodePacket我们都没有假定编码后的数据是发送到socket,还是从内存读取数据解码,因此我们可以这样调用EncodePacket

把数据直接发送到socket,也可以这样

对socket加上一个buffer来增加吞吐量,也可以这样

加上一个zip压缩,还可以利用加上crypto/aes来个AES加密…

在这个时候,文件已经不再局限于io,可以是一个内存buffer,也可以是一个计算hash的对象,甚至是一个计数器,流量限速器。golang灵活的接口机制为我们提供了无限可能。

结尾

我一直认为一个好的语言一定有一个设计良好的标准库,golang的标准库是作者们多年系统编程的沉淀,值得我们细细品味

%e6%a0%87%e5%bf%97_meitu_1_meitu_2

文章分类 go
One comment on “golang IO包的妙用
  1. 增达网说道:

    受教了!呵呵!

发表评论

电子邮件地址不会被公开。

在线交流

数百位业内高手和同行在等你交流
Reboot运维开发分享