go I/O 操作

理解 I/O 的一些概念问题可以先看另一篇文章《C Socket 编程》 Go 语言把 I/O 操作抽象成为 Reader 和 Writer 接口,并在 C 语言的基础上又设置了一层缓存操作。

1   简介

先进行接口设计,后进行实现设计,即对每一个接口方法进行设计。

1.1   接口设计

定义一个接口,是要设计一种方法集。在做接口设计的时候,每一个方法的具体功能一定要能表述出来;并且根据所有方法,其中可以汇总出核心必备的属性,当然接口不能拥有属性,这只是一种脑补的属性,有利于实现设计。

可以分析 io 的接口实现,包括所有接口的功能描述和脑补核心属性:

  • io.Reader:只有一个 Read 方法
  1. Read 方法的功能:可以从中读取数据,可以分多次读取
  2. 脑补核心属性:有两个,一个是源比如字符串、文件等;另一个是对源的当前读取到的指向
  • io.Writer:只有一个 Write 方法
  1. Write 方法的功能:可以把数据写进去,能写多少依据源的能力
  2. 脑补核心属性:直接一个源就可以了

1.2   实现设计

根据要实现的方法集的一种设计,并可以由此知道需要的属性。原则上对每一个方法,如果有输出的话,则无论采用哪种设计,对同样的输入(或者同样的无输入),应该要有同样的输出。 实现其实就是一种设计(当然也可以扩充其功能),比如 strings.Reader 基本上就是比较原始的设计,而 bufio.Reader 采用了缓冲的设计实现了同样的功能。接下来我们列举一些常用的实现,并分析其原理。

1.3   io 包

io 包中定义有:

  • 大量的接口
  • 一些通用函数
  • 小量的接口实现,在原io中的实现并不常用,strings、bufio 等包对 io 的实现更常用一些。

总体来看,io.go 主要是进行接口设计,实现设计是次要的。

2   bufio 实现设计

bufio.Reader 封装了 io.Reader,bufio.Writer 封装了 io.Writer,接下来就分别从这两个类型进行讲解。

2.1   bufio.Reader

bufio 封装了对应 io 的同时多了 buf 属性,和对 buf 的控制r、w这两个属性,读和写其实就会优先从 buf 进行,

bufio.Reader 主要有buf、r、w、rd(io.Reader)等属性(其它属性对原理的理解相对次要所以不提),把读操作优化成“rd->buf->变量”,除了一些特殊情况还是保留“rd->变量”的读取方式。 读取主要分两类:1、确定读取长度,如bufio.Read,bufio.ReadByte等;2、确定读取到某个字符,如bufio.ReadSlice,bufio.ReadLine,bufio.ReadBytes,bufio.ReadString等

  1. bufio.Read:
  • 如果 buf 不为空,则从 buf 取数据尽可能多的把 p 填满(可能填不满,这种情况下即使io.Reader还有未读数据,也不会再去取,也就是只取 buf 数据)。
  • 如果 buf 为空,即 r==w,则判断要取的长度是否大于buf,如果大于等于buf,再直接从io.Reader取,也只有这种情况下是直接取的即“rd->变量”;如果小于 buf,则把buf填满,然后回到1;
  1. ReadSlice 或 ReadLine 该两个方法比较底层,不建议使用,这里需要注意的,返回的其实是指向 bufio.buf 属性的切片,因为 bufio.buf 底层的数组指针一直不变,而值却在变,因为返回的切片区域可能会因为 bufio.buf 值的改变而改变,特别在多次调用时要注意。

  2. ReadBytes 或 ReadString 这两个方法通过调用 ReadSlice 实现,并在最终 copy 到新创建的切片返回,所以多次读取很安全。

  3. WriteTo 它的功能是把全部数据写到 Writer,对于 bufio,必须分两步:

  • 第一步把当前的缓冲写到 Writer,内部函数 writeBuf 就是单纯把缓冲区写到 Writer
  • 然后把还没有缓冲的问题写到 Writer,判断逻辑是:源 Reader 有 WriterTo 就调用其 WriterTo,没有就走 fill 填充(也就是调用源 Reader 的 Read 读取)

2.2   bufio.Writer

如果写入的数据大于缓冲区,则直接写入。 写入只是写到缓冲区,注意需要调用 Flush 方法写入,否则即使是程序运行结束也不会写入。

  1. ReadFrom
  • 如果缓冲为空且源 ReaderFrom 存在,则直接从源里调用
  • 否则循环的取到缓冲中,并 Flush

3   bytes.buffer 实现设计

主要属性有 buf []byte 和 off:read at &buf[off], write at &buf[len(buf)]

4   Socket 实现设计

首先我们看一下一段 TCP 代码 socker.go

net.Dial 和 net.Listen().Accept() 返回 socket,可读可写,本质上是经过以下步骤取得文件句柄的: net.Listen().fd.accept() 取得 fd 句柄,然后创建 net.conn(实际上 socket 还要经过协议一层包装)

type conn struct {
	fd *netFD
}

抓包分析:以代码的逻辑,TCP 数据一定是完整传输的,且每次发送的时间都有 PSH 标识;但因为并发的原因,服务端仅一次且不确定时间地读取 TCP 数据,导致在服务端读取之后发送的数据没有继续读取。 发送与接收

4   ioutil 工具