你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

上篇文章 ​​Go bufio.Reader 结构+源码详解 I​​​,我们介绍了 ​​bufio.Reader​​,bufio.Reader 利用一个缓冲区,在底层文件读取器和读操作方法间架起了桥梁。有这样一个缓冲区的好处是,可以在大多数的时候降低读取方法的执行时间。虽然,读取方法有时还要负责填充缓冲区,但从总体来看,读取方法的平均执行时间一般都会因此有大幅度的缩短。本篇文章,我们将继续阅读源码,学习方法的底层实现,做到知己知彼,才能运用自如。

Go bufio.Reader 结构+源码详解 II_读取数据

Peek

Peek方法用于查看未读数据的前n个字节,该方法并不会更改 bufio.Reader 的状态,不会更新已读计数,同时该方法不属于读取操作,不能用于后续的回退操作。

需要注意的是,该方法返回的是缓冲区的切片,可能造成数据泄露的风险,因为调用者可以通过返回的切片直接修改缓冲区的值;其次,返回数据的有效期是在下次数据读取之前,因为下次读取数据可能会数据压缩平移,导致当前数据的位置被改变。

func (b *Reader) Peek(n int) ([]byte, error) {

// 非法参数
if n < 0 {
return nil, ErrNegativeCount
}

// peek方法会使得回退操作失效
b.lastByte = -1
b.lastRuneSize = -1

// 未读数据长度小于所需长度 n ,且缓冲区未满,那么将缓冲区填满
for b.w-b.r < n && b.w-b.r < len(b.buf) && b.err == nil {
b.fill()
}

// n 大于缓冲区长度,返回所有有效数据 以及 ErrBufferFull error
if n > len(b.buf) {
return b.buf[b.r:b.w], ErrBufferFull
}

// 此时 0 <= n <= len(b.buf),即 n 小于缓冲区长度
// 1. 如果有效数据长度小于 n,说明之前的 fill 方法没有将缓冲区填满,那么此时最多只能返回所有的有效数据,
// 并返回 fill 方法产生的error
// 2. 如果有效数据长度大于 n,就返回前n个有效数据
var err error
if avail := b.w - b.r; avail < n {
// 有效数据不足,设置n为最大的有效数据长度
n = avail
err = b.readErr()
if err == nil {
err = ErrBufferFull
}
}
return b.buf[b.r : b.r+n], err
}

Discard

Discard方法 会丢弃缓冲区的n个字节,最后返回实际丢弃的字节数和产生的 error。

对于合法参数 n,方法使用 for 循环不断装填数据,来尽量满足丢弃 n 个字节。即如果有效数据长度小于 n 的话,丢弃现有数据后,再重新调用fill 方法,填充新的数据用于丢弃,如果在这个过程中遇到err,方法就终止,最终返回实际丢弃的字节数和遇到的error。如果 buf 可丢弃的有效字节数大于 n,丢弃部分字节即可。

func (b *Reader) Discard(n int) (discarded int, err error) {

// 非法参数
if n < 0 {
return 0, ErrNegativeCount
}

// 0表示不丢弃数据,直接返回
if n == 0 {
return
}

// remain 表示还需丢弃多少字节,开始时剩余n个字节待丢弃
remain := n

// 如果传入的 n 很大,要丢弃很多字节,但是缓冲区的有效数据长度不满足要求,需要多次丢弃
for {

// skip 表示当前可以丢弃的的有效字节长度
skip := b.Buffered()

// 如果当前缓冲区的有效数据长度为 0,调用 fill 方法填充
if skip == 0 {
b.fill()
skip = b.Buffered()
}

// 如果当前有效数据长度大于待丢弃字节数,只需跳过待丢弃字节数即可
if skip > remain {
skip = remain
}

// 已读计数增加 skip 个值,表示丢弃 skip 个字节
b.r += skip

// 更新剩余待丢弃字节数
remain -= skip

// 如果待丢弃字节数为0,说明完成了任务,直接返回
if remain == 0 {
return n, nil
}

// 产生了 error,返回已经丢弃的字节数,以及 error
if b.err != nil {
return n - remain, b.readErr()
}

// 到这里说明 remain>0,且b.err==nil,需要继续丢弃
}
}

Read

Read 方法读取数据到 字节切片 ​​p​​ 中,返回读取的字节数和产生的 error。

  • 当缓冲区有效数据不为空时,直接将缓冲区的有效数据复制到字节切片p中,有多少就写入多少,不会再读取底层数据填充,因此如果当前缓冲区的有效数据长度小于传入字节切片 p 的长度,读取的字节数 n < len(p);
  • 当缓冲区有效数据为空时,从底层文件读取数据,填充字节切片p。
  • 当 p 的长度小于缓冲区长度时,从底层读取​​一次 ​​ 数据到缓冲区,然后将缓冲区的数据复制到 p 中
  • 当 p 的长度大于缓冲区长度时,有一个优化,不会先写入缓冲区再复制到 p,这种方式不仅多复制一次,读取的数据还少于想要的数据长度,而是直接读取底层数据到 p 中,简单高效。

从上面分析来看,Read 方法至多只会从底层数据读取器中读取一次数据,因此读取的数据长度会小于 len(p),如果想要保证放回的数据长度等于 len(p),使用 ​​io.ReadFull(b,p)​

func (b *Reader) Read(p []byte) (n int, err error) {
n = len(p)

// 传入的字节切片长度为0,看当前缓存数据长度是否大于0,决定是否返回 err
if n == 0 {
if b.Buffered() > 0 {
return 0, nil
}
return 0, b.readErr()
}

// len(p) > 0,缓冲区有效数据为0
if b.r == b.w {

// 缓存数据为0,可能 err!=nil
if b.err != nil {
return 0, b.readErr()
}

// 传入的字节切片长度大于缓冲区长度,且缓冲区无有效数据
if len(p) >= len(b.buf) {

// 直接从底层文件读取数据,写入到 p 中,而不是先写到缓冲区再复制到 p 中,复制浪费时间,数据还较少
n, b.err = b.rd.Read(p)

if n < 0 {
panic(errNegativeRead)
}

// 读到了数据,更新回退
if n > 0 {
b.lastByte = int(p[n-1])
b.lastRuneSize = -1
}

// 返回
return n, b.readErr()
}

// 到这里说明缓冲区为空,且len(p) < len(b.buf)

// 更新已读计数和已写计数为0,压缩无效数据,然后只进行一次数据读取,写入到缓冲区
b.r = 0
b.w = 0
n, b.err = b.rd.Read(b.buf)
if n < 0 {
panic(errNegativeRead)
}
if n == 0 {
return 0, b.readErr()
}

// 更新已写计数
b.w += n
}

// 复制有效数据到 p 中
n = copy(p, b.buf[b.r:b.w])
b.r += n
b.lastByte = int(b.buf[b.r-1])
b.lastRuneSize = -1
return n, nil
}

ReadByte

ReadByte方法读取一个字节,返回读取的字节和产生的 Error。

如果缓冲区的有效数据为空,会不断尝试调用 ​​fill​​ 方法填充数据,然后返回缓冲区有效数据的第一个字节;如果调用 ​​fill​​ 方法产生 error,则会返回error。

func (b *Reader) ReadByte() (byte, error) {
b.lastRuneSize = -1

// 缓冲区有效数据为空,会一直尝试填充数据,直至遇到 err!=nil,或者成功填充数据
for b.r == b.w {
if b.err != nil {
return 0, b.readErr()
}

// 填充数据
b.fill()
}

// 有效数据部分的第一个字节
c := b.buf[b.r]

// 已读计数加一
b.r++

// 保存刚读取的这个字节,用于之后的回退操作
b.lastByte = int(c)
return c, nil
}

UnreadByte

UnreadByte方法 用于回退读操作,即把上一次读操作的最后一个字节置为未读,下次读取的话,该字节是第一个被读取的字节。如果上一个的操作不是读操作,lastByte 会被置为 -1,就不能完成回退操作 (Peek方法不算做读操作)。

func (b *Reader) UnreadByte() error {

// lastByte < 0 说明上一次不是读操作,不能回退
// b.r == 0 && b.w > 0,压缩后没有进行读操作,会出现这种情况,没有已读数据,不能回退

if b.lastByte < 0 || b.r == 0 && b.w > 0 {
return ErrInvalidUnreadByte
}
// b.r > 0 || b.w == 0
if b.r > 0 {
b.r--
} else {
// b.r == 0 && b.w == 0
b.w = 1
}
b.buf[b.r] = byte(b.lastByte)
b.lastByte = -1
b.lastRuneSize = -1
return nil
}
func (b *Reader) UnreadByte() error {   // lastByte < 0 说明上一次不是读操作,不能回退 // b.r == 0 && b.w > 0,压缩后没有进行读操作,会出现这种情况,没有已读数据,不能回退   if b.lastByte < 0 || b.r == 0 && b.w > 0 {      return ErrInvalidUnreadByte }   // b.r > 0 || b.w == 0  if b.r > 0 {        b.r--   } else {        // b.r == 0 && b.w == 0     b.w = 1 }   b.buf[b.r] = byte(b.lastByte)   b.lastByte = -1 b.lastRuneSize = -1 return nil}复制代码

总结

本篇文章我们介绍了 ​​bufio.Reader​​ 的如下几个方法:

  • Peek:查看部分数据,但是不改变结构体的状态
  • Discard:丢弃数据
  • Read:读取数据,同时针对缓冲区为空的其中一个情形做了优化,直接从底层文件读取,不经过缓冲区
  • ReadByte:读取一个字节
  • UnreadByte:回退一个字节

更多

个人博客: ​​lifelmy.github.io/​

微信公众号:漫漫Coding路