Golang IO包的妙用

简介:

背景

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

type Packet struct {
   TotalSize uint32    
   Magic     [4]byte    
   Payload   []byte    
   Checksum  uint32 }

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

Checksum是对MagicPayloadadler32校验和。

编码(encode)

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

var RPC_MAGIC = [4]byte{'p', 'y', 'x', 'i'}

func EncodePacket(w io.Writer, payload []byte) error {
   // len(Magic) + len(Checksum) == 8    totalsize := uint32(len(payload) + 8)    
   // write total size    binary.Write(w, binary.BigEndian, totalsize)    
   
   // write magic bytes    binary.Write(w, binary.BigEndian, RPC_MAGIC)    
   
   // write payload    w.Write(payload)    
   
   // calculate checksum    var buf bytes.Buffer    buf.Write(RPC_MAGIC[:])    buf.Write(payload)    checksum := adler32.Checksum(buf.Bytes())    
   
   // write checksum    return binary.Write(w, binary.BigEndian, checksum) }

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

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

type Hash interface {
   // Write (via the embedded io.Writer interface) adds more data to the running hash.    // It never returns an error.    io.Writer    
   
   // Sum appends the current hash to b and returns the resulting slice.    // It does not change the underlying hash state.    Sum(b []byte) []byte    // Reset resets the Hash to its initial state.    Reset()    
   
   // Size returns the number of bytes Sum will return.    Size() int    // BlockSize returns the hash's underlying block size.    // The Write method must be able to accept any amount    // of data, but it may operate more efficiently if all writes    // are a multiple of the block size.    BlockSize() int
}

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

func EncodePacket2(w io.Writer, payload []byte) error {
   // len(Magic) + len(Checksum) == 8    totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)    
   // write total size    binary.Write(w, binary.BigEndian, totalsize)    
   
   // write magic bytes    binary.Write(w, binary.BigEndian, RPC_MAGIC)    
   
   // write payload    w.Write(payload)  
   
   // calculate checksum    sum := adler32.New()    sum.Write(RPC_MAGIC[:])    sum.Write(payload)    checksum := sum.Sum32()    
   
   // write checksum    return binary.Write(w, binary.BigEndian, checksum) }

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

考虑到sumw都是io.Writer,利用神奇的 io.MultiWriter  (https://godoc.org/io#MultiWriter),我们可以这样写:

func EncodePacket(w io.Writer, payload []byte) error {
   // len(Magic) + len(Checksum) == 8    totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)    
   // write total size    binary.Write(w, binary.BigEndian, totalsize)    sum := adler32.New()    ww := io.MultiWriter(sum, w)    
   // write magic bytes    binary.Write(ww, binary.BigEndian, RPC_MAGIC)    
 
   // write payload    ww.Write(payload)    
 
  // calculate checksum    checksum := sum.Sum32()  
 
  // write checksum    return binary.Write(w, binary.BigEndian, checksum) }

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

解码(decode)

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

func DecodePacket(r io.Reader) ([]byte, error) {
   var totalsize uint32    err := binary.Read(r, binary.BigEndian, &totalsize)    
   if err != nil {    
       return nil, errors.Annotate(err, "read total size")    }    
       
   // at least len(magic) + len(checksum)    if totalsize < 8 {    
       return nil, errors.Errorf("bad packet. header:%d", totalsize)    }    sum := adler32.New()    rr := io.TeeReader(r, sum)    
   
   var magic [4]byte    err = binary.Read(rr, binary.BigEndian, &magic)    
   if err != nil {    
       return nil, errors.Annotate(err, "read magic")    }    
   if magic != RPC_MAGIC {    
       return nil, errors.Errorf("bad rpc magic:%v", magic)    }    payload := make([]byte, totalsize-8)    _, err = io.ReadFull(rr, payload)    
   if err != nil {    
       return nil, errors.Annotate(err, "read payload")    }    
   
   var checksum uint32    err = binary.Read(r, binary.BigEndian, &checksum)    
   if err != nil {    
       return nil, errors.Annotate(err, "read checksum")    }    
       
   if checksum != sum.Sum32() {    
       return nil, errors.Errorf("checkSum error, %d(calc) %d(remote)", sum.Sum32(), checksum)    }    
   return payload, nil
}

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

一切皆文件

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

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
EncodePacket(conn, []byte("hello"))

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

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))

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

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
zip := zlib.NewWriter(conn)
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))

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

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

END

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


本文转自 xjtuhit 51CTO博客,原文链接:http://blog.51cto.com/51reboot/1953255
相关文章
|
8月前
|
Java 编译器 Go
【Golang】(1)Go的运行流程步骤与包的概念
初次上手Go语言!先来了解它的运行流程吧! 在Go中对包的概念又有怎样不同的见解呢?
415 4
|
11月前
|
设计模式 Kubernetes Go
​​什么是Golang项目的“主包精简,逻辑外置”?​
“主包精简,逻辑外置”是Go语言项目的一种设计原则,强调将程序入口保持简单,核心逻辑拆分至其他包,以提升代码可维护性、可测试性及扩展性,适用于CLI工具、Web服务等场景。
264 7
|
Go
Golang的math包常用方法
这篇文章介绍了Golang的math包中的常量和常用方法,并通过示例代码展示了如何使用这些常量和方法。
467 87
Golang的math包常用方法
|
Go
Golang语言之包依赖管理
这篇文章详细介绍了Go语言的包依赖管理工具,包括godep和go module的使用,以及如何在项目中使用go module进行依赖管理,还探讨了如何导入本地包和第三方库下载的软件包存放位置。
519 4
|
存储 Go
Golang语言基于go module方式管理包(package)
这篇文章详细介绍了Golang语言中基于go module方式管理包(package)的方法,包括Go Modules的发展历史、go module的介绍、常用命令和操作步骤,并通过代码示例展示了如何初始化项目、引入第三方包、组织代码结构以及运行测试。
799 3
|
Go
Golang语言基于GOPATH方式管理包(package)
这篇文章详细介绍了Golang语言中基于GOPATH方式管理包(package)的方法,包括包的概述、定义、引入格式、别名使用、匿名引入,以及如何快速入门自定义包,并通过具体代码案例展示了包的环境准备、代码编写、细节说明和程序运行。
324 3
|
机器学习/深度学习 存储 人工智能
Golang bytes 包学习
Golang bytes 包学习
208 3
|
Go 开发者
什么是 Golang 包?详解 Go 语言的包系统
【8月更文挑战第31天】
501 0
|
存储 测试技术 Go
Golang 包:构建模块化代码的基石
【8月更文挑战第31天】
310 0
|
监控 Go 开发者
Golang深入浅出之-Goroutine泄漏检测与避免:pprof与debug包
【5月更文挑战第2天】本文介绍了Go语言并发编程中可能遇到的Goroutine泄漏问题,以及如何使用`pprof`和`debug`包来检测和防止这种泄漏。常见的问题包括忘记关闭channel和无限制创建goroutine。检测方法包括启动pprof服务器以监控Goroutine数量,使用`debug.Stack()`检查堆栈,以及确保每个Goroutine有明确的结束条件。通过这些手段,开发者可以有效管理Goroutine,维持程序性能。
645 7