bufio是buffered I/O的缩写,这个代码包的程序实体实现的I/O操作都内置了缓冲区。
bufio包中的数据类型主要有:
这些数据类型在初始化的时候需要包装一个或多个简单I/O接口类型的值。
bufio.Reader
bufio.Reader
中缓冲区的作用bufio.Reader
中的缓冲区,其实就是一个数据存储中介,它介于底层读取器与读取方法及调用方之间。
所谓的底层读取器,就是在初始化此类值的时候传入的io.Reader类型的参数值。
bufio.Reader
类型值的读取方法一般都会先从其所属值的缓冲区读取数据。同时,在必要的时候,它们还会预先从底层读取器里读出一部分数据,并暂存于缓冲区中以备后用。
这样的一个缓冲区的好处是:可以在大多数的时候降低读取方法的执行时间。
虽然读取方法有时还要负责填充缓冲区,但从总体来看,读取方法的平均执行时间一般都会因此有大幅度的缩短。
bufio.Reader
类型值的初始化字段buf
:[]byte
类型的字段,即字节切片,代表缓冲区。
虽然是切片类型,但是其长度却会在初始化的时候指定,并在之后保持不变。
rd
:io.Reader
类型的字段,代表底层读取器。
缓冲区的数据就是从这里拷贝来的。
r
:int类型的字段,代表缓冲区进行下一次读取时的开始索引。也称为已读计数。
w
:int类型的字段,代表缓冲区进行下一次写入时的开始索引。也称为已写计数。
err
:error类型的字段。它的值表示从底层读取器获得数据时发生的错误。
lastByte
:int类型的字段,用于记录缓冲区中最后一次被读取的字节。
读回退时会用到它的值。
lastRuneSize
:int类型的字段。用于记录缓冲区中最后一个被读取的Unicode字符所占用的字节数。
读回退时会用到它的值。这个字段只会在其所属值的ReadRune方法中才会被赋予有意义的值。在其它的情况下,它都会被置为-1。
bufio.Reader
类型值的函数bufio
包提供了两个用于初始化bufio.Reader
值的函数:
NewReader
NewReader
函数初始化bufio.Reader
类型的值会拥有一个默认尺寸的缓冲区。这个默认尺寸是4096个字节,即4KB。
NewReaderSize
NewReaderSize
将缓冲区尺寸的决定权交给使用方。
它们都返回一个*bufio.Reader
类型的值。
bufio.Reader
的包级私有方法fill
在bufio.Reader
类型拥有的读取方法中,Peek方法和ReadSlice方法都会调用该类型一个名为fill的包级私有方法。
fill
方法的作用是填充内部缓冲区。
fill
方法的工作机制检查其所属值的已读计数。如果这个计数等于0,那么有两种可能。
第一种情况:缓冲区没有被读取过,缓冲区的字节都是全新的。
第二种情况:缓冲区被压缩过。
压缩缓冲区
第一步:把缓冲区中在[已读计数,已写计数)范围之内的所有元素值(或者说字节)都依次拷贝到缓冲区的头部。
注意,是拷贝。原有位置的元素值还在。
第二步:fill方法会把已写计数的新值设定为原已写计数与已读计数的差。
这个差所代表的索引,就是压缩后第一次吸入字节时的开始索引。
另外,该方法还会把已读计数的值置为0。
压缩之后,再读取字节就肯定要从缓冲区的头部开始读了。
压缩缓冲区的时机
fill方法只要在开始时发现其所属值的已读计数大于0,就会对缓冲区进行一次压缩。
填充缓冲区
如果缓冲区还有可写的位置,fill方法就会对其进行填充。fill方法会从底层读取器那里,读取足够多的字节,并尽量把从已写计数代表的索引位置到缓冲区末尾之间的空间都填满。
这个过程会不断更新已写计数。如果过程中发生错误,会把错误赋给err。
bufio.Reader
类型的读取方法bufio.Reader
有多个用于读取数据的指针方法:其中有4个方法可以作为不同读取流程的代表:
Peek
方法func (b *Reader) Peek(n int) ([]byte, error)
bufio.Reader
类型值的Peek方法的功能是:读取并返回缓存区中的n个未读字节,并且它会从已读计数代表的索引位置开始读。
在缓存区未被填满,并且其中的未读字节的数量小于n的时候,该方法会调用fill方法,以启动缓存区的填充流程。但是,如果发现上次填充缓冲区的时候有错误,那就不会再次填充了。
如果调用方给定的n比缓冲区还要大,或者缓冲区中未读字节的数量小于n。那么Peek方法就会把“所有未读字节组成的序列”作为第一个结果值返回。同时,它通常还把bufio.ErrBufferFull
变量的值作为第二个结果值返回。
bufio.ErrBufferFull
表示:虽然缓冲区被压缩或填满了,但是仍然满足不了要求。
只有上面两种情况都未出现,Peek方法才会返回:“以未读计数为起始的n个字节”和表示未发生任何错误的nil。
bufio.Reader
类型值有一个鲜明特点:即使它读了缓冲区的数据,也不会更改已读计数的值。
Read
方法 p := make([]byte, 10)
br01.Read(p)
bufio.Reader
类型值的Read方法:
在缓冲区还有未读字节的时候,会把缓冲区的未读字节,依次拷贝到其参数p代表的字节切片中。并立即根据实际拷贝的字节增加已读计数的值。
当缓冲区没有未读字节,Read方法会检查参数p的长度是否大于或等于缓冲区的长度。
如果是,那么Read方法会索性放弃向缓冲区中填充数据,转而直接从其底层读取器中读出数据并拷贝到p中。
这意味着它跨越了缓冲区,并直连了数据供需的双方。
如果缓冲区已无未读字节,并且缓冲区的长度比参数p的长度更大。该方法会先把已读计数和已写计数的值都重置为0。然后冲底层读取器那里获取数据,对缓冲区进行一次从头到尾的填充。
Read
方法和Peek
方法相同的一点是:只要他们把获得的数据写入缓冲区,就会及时的更新已写计数的值。
ReadSlice
方法(b *Reader) ReadSlice(delim byte) (line []byte, err error)
。
ReadSlice方法会持续地读取数据,直至调用方给定的分隔符为止。
ReadSlice方法的运行原理:
ReadSlice方法会现在缓冲区的未读部分寻找分隔符。
如果未能找到,并且缓冲区未被填满,那么该方法会通过调用fill方法对缓冲区进行填充,然后寻找。如此反复。
如果在填充缓冲区的过程中发生错误,那么它会把缓冲区中未读部分作为结果返回,同时返回相应的错误值。
如果缓冲区已经被填满,但仍然没有找到分隔符,ReadSlice方法就会把整个缓冲区作为第一个结果值(也就是buf代表的字节切片),并把缓冲区已满作为第二个结果值(即bufio.ErrBufferFull变量的值
)。
经过fill填满缓冲区,肯定从头到尾都只包含未读的字节。
一旦ReadSlice找到了分隔符,他就会在缓冲区上切出相应的、包含分隔符的字节切片,并把该切片作为结果值返回。
如果分隔符是否找到,该方法都会正确地设置已读计数的值。
ReadBytes
方法(依赖ReadSlice方法)func (b *Reader) ReadBytes(delim byte) ([]byte, error)
。
ReadBytes 方法会持续地读取数据,直至调用方给定的分隔符为止。
ReadBytes方法的运行原理:
其它依赖ReadSlice的方法:ReadLIne。
依赖ReadBytes的方法:ReadString。
bufio.Reader
类型的Peek方法、ReadSlice方法、ReadLine方法都可能造成内存泄露。
内存泄露:读取到不该读到的内容。
bufio.Writer
bufio.Writer
类型的字段err
:error类型的字段。它的值表示在向底层写入器写入数据时发生的错误。buf
:[]byte
类型的字段,代表缓冲区。在初始化之后,它的长度会保持不变。n
:int类型的字段,代表对缓冲区进行下一次写入时的开始索引。我们可以称之为已写计数。wr
:io.Writer
类型的字段,代表底层写入器。bufio.Writer
类型值中缓冲的数据何时写入底层写入器bufio.Writer
类型有一个名为Flush的方法,它的主要功能是把相应缓冲区中暂存的所有数据,都写到底层写入器中。数据一旦被写入底层写入器,该方法就会把它们从缓冲区中删除掉。
Flush方法会保证不会出现重写和漏写到情况,该类型的字段n会起到很重要的作用。
Write
和WriteString
方法,有时候会调用Flush方法,以便为后续的新数据腾出空间。
WriteByte
和WriteRune
方法,会在发现缓冲空间不足以容纳新的字节或Unicode字符的时候,调用Flush方法。
此外,Write
方法如果发现要写入的字节太多,并且缓存区为空的话,那么它会直接跨越缓冲区,并直接把这些数据写到底层写入器中。
ReadFrom
方法,会在发现底层写入器的类型是io.ReaderFrom
接口的实现之后,直接调用ReadFrom方法把参数值持有的数据写进去。
当把所有数据都写入Writer值之后,再调用一下Flush方法,是最稳妥的。
buffio.Scanner
思考。