Golang IO包的妙用

时间:2022-09-04 08:48:29

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 灵活的接口机制为我们提供了无限可能。

结尾

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

阅读原文

Golang IO包的妙用的更多相关文章

  1. 简析 Golang IO 包

    简析 Golang IO 包 io 包提供了 I/O 原语(primitives)的基本接口.io 包中定义了四个最基本接口 Reader.Writer.Closer.Seeker 用于表示二进制流的 ...

  2. golang io中io&period;go解读

    目录 1. 整体大纲 2. 接口 读 写 关闭 寻址 3. 函数 读 写 复制 4. 结构体 SectionReader LimitedReader teeReader 5. 备注 根据golang ...

  3. Golang学习 - io 包

    ------------------------------------------------------------ 先说一下接口,Go 语言中的接口很简单,在 Go 语言的 io 包中有这样一个 ...

  4. Golang Vendor 包管理工具 glide 使用教程

    Glide 是 Golang 的 Vendor 包管理器,方便你管理 vendor 和 verdor 包.类似 Java 的 Maven,PHP 的 Composer. Github:https:// ...

  5. Go package&lpar;3&rpar;:io包介绍和使用

    IO 操作的基本分类 在计算机中,处理文件和网络通讯等,都需要进行 IO 操作,IO 即是 input/ouput,计算机的输入输出操作. Go语言中的 IO 操作封装在如下几个包中: io 为 IO ...

  6. java&period;io包详细解说

    转自:http://hzxdark.iteye.com/blog/40133 hzxdark的博客 我不知道各位是师弟师妹们学java时是怎样的,就我的刚学java时的感觉,java.io包是最让我感 ...

  7. java&period;io包中的字节流—— FilterInputStream和FilterOutputStream

    接着上篇文章,本篇继续说java.io包中的字节流.按照前篇文章所说,java.io包中的字节流中的类关系有用到GoF<设计模式>中的装饰者模式,而这正体现在FilterInputStre ...

  8. 黑马程序员——【Java基础】——File类、Properties集合、IO包中的其他类

    ---------- android培训.java培训.期待与您交流! ---------- 一.File类 (一)概述 1.File类:文件和目录路径名的抽象表现形式 2.作用: (1)用来将文件或 ...

  9. apache commons io包基本功能

    1. http://jackyrong.iteye.com/blog/2153812 2. http://www.javacodegeeks.com/2014/10/apache-commons-io ...

随机推荐

  1. C&plus;&plus; 中的对象布局

    C++中的涉及到虚表时,类对象的布局分为:虚表与数据成员,子类包含派生类布局,假设下面一个程序: #include <iostream> using namespace std; clas ...

  2. HDU4725 The Shortest Path in Nya Graph SPFA最短路

    典型的最短路问题,但是多了一个条件,就是每个点属于一个layer,相邻的layer移动,如x层移到x+1层需要花费c. 一种显而易见的转化是我把这些边都建出来,但是最后可能会使得边变成O(n^2); ...

  3. jsp链接数据库

    数据库表代码: /*Navicat MySQL Data Transfer Source Server : localhost_3306Source Server Version : 50528Sou ...

  4. Windows环境下使用cygwin ndk&lowbar;r9c编译x264

     一.废话 最近学习,第一步就是编译.我们需要编译FFmpag,x264,fdk_aac,下面是x264,网上说的很多都是几百年前的,我亲测完美可用 还是那句话 我能力有限,但是我希望我写的东西能够让 ...

  5. android小知识之邮箱地址输入自动完成

    虽然不难,但是容易忘记,做个备忘吧 package com.guet.zhuge; import android.app.Activity; import android.os.Bundle; imp ...

  6. 快速检查SQL两表数据是否一致

    1前话 项目内实现了一新功能:克隆数据库. 2目标 克隆并非用SQLSERVER克隆,故完毕后需要检查各表内一些数据与原表一致性.一些表中的某一些列容许不一致. 3实现 将两表的需要检查的几列取出,相 ...

  7. jquery选择器总结2

    1.JQuery的概念 JQuery是一个JavaScript的类库,这个类库集合了很多功能方法,利用类库你可以用一些简单的代码实现一些复杂的JS效果. 2.JQuery实现了 代码的分离 不用再网页 ...

  8. 采集的时候&comma;列表的编码是gb2312&comma;内容页的编码却是UTF-8,这种网站怎么采集?

    采集的时候,列表的编码是gb2312,内容页的编码却是UTF-8,这种网站怎么采集? 采集的时候,列表的编码是UTF-8,内容页的编码却是gb2312,这种网站怎么采集? 这种情况怎么解决呢? 哈哈哈 ...

  9. Am335x 应用层之SPI操作

    SPI接口有四种不同的数据传输时序,取决于CPOL和CPHL这两位的组合.图1中表现了这四种时序, 时序与CPOL.CPHL的关系也可以从图中看出. 图1 CPOL是用来决定SCK时钟信号空闲时的电平 ...

  10. Smarty模板的引用

    (1)include用法和php里的include差不多(2)smarty的include还具备自定义属性的功能例如 {include file="header.tpl" titl ...