Go bufio.Reader 结构+源码详解 I

1,028 阅读6分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

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

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

前言

前面的两篇文章 Go 语言 bytes.Buffer 源码详解之1Go 语言 bytes.Buffer 源码详解 2,我们介绍了 bytes.buffer,它是一个字节缓冲区,我们可以将数据先写到到缓冲区再进行处理。但是 bytes.buffer 并没有提供对底层文件操作的相关接口(ReadFrom 会将整个文件内容写入缓冲区,不适用于大文件),如果想要对文件进行操作,需要我们手动读取文件内容写入缓冲区,不免有些麻烦。

我们都知道,对文件的IO操作,是比较费时的。如果每操作一次数据就要读取一下文件,IO操作是非常多的。那么如何提高效率呢?可以考虑预加载,读取数据的时候,提前加载部分数据到缓冲区中,如果缓冲区长度大于每次要操作的数据长度,这样就减少了 IO 次数;同样,对于写文件,我们可以先将要写入的数据存入缓冲区,然后一次性将数据写入文件。

bufio包 基于缓冲区,提供了便捷的文件IO操作方法,并利用缓冲区减少了IO次数,本篇文章就先来学习文件读取相关结构 bufil.Reader。

结构总览

bufio.Reader 利用一个缓冲区,在底层文件读取器和读操作方法间架起了桥梁。底层文件读取器就是初始化 Reader 的时候需要传入的io.Reader。有这样一个缓冲区的好处是,每次我们想读取文件内容时,会首先从缓冲区读取,提高了读取速度,也避免了频繁的 文件IO,同时必要时会利用底层文件读取器提前加载部分数据到缓冲区中,做到未雨绸缪。

有这样一个缓冲区的好处是,可以在大多数的时候降低读取方法的执行时间。虽然,读取方法有时还要负责填充缓冲区,但从总体来看,读取方法的平均执行时间一般都会因此有大幅度的缩短。

bufio.Reader 的结构如下:

bufio.Reader中的 r、w 分别代表当前读取和写入的位置,读写都是针对缓存切片 buf 来说的,io.Reader rd 是用来写入数据到 buf 的,因此当写入了部分字节,w 会增大相应的写入字节数;而当从 buf 中读出数据后,r 会增大,被读取过的数据就是无用数据了。始终 w>=r,当 w==r 时,说明写入的数据都被读取完毕了,没有数据可读了。

bufio.Reader结构

  • buf:用作缓冲区的字节切片,虽然是切片类型,但是一旦初始化完成之后,长度不会改变
  • rd:初始化时传入的io.Reader,用于读取底层文件数据,然后写入到缓冲区 buf 中
  • r:下一次读取缓冲区 buf 时的起始位置,即 r 之前的数据都是被读取过的,下次读取重从 r 位置开始,我们称之为已读计数
  • w:下一次写入缓冲区 buf 时的起始位置,即 w 之前都是之前写过的数据,下次写入从 w 位置开始,我们称之为已写计数
  • err:记录 rd 读取数据时产生的 error,err 在被读取或忽略之后,会被置为nil
  • lastByte:保存上一次读取的最后一个字节的位置,用于回退一个字节;-1 表示无效值,不能回退
  • lastRuneSize:保存上一次读取的 rune 的位置,用于回退一个rune;-1 表示无效值,不能回退
type Reader struct {
	buf          []byte
	rd           io.Reader // reader provided by the client
	r, w         int       // buf read and write positions
	err          error
	lastByte     int // last byte read for UnreadByte; -1 means invalid
	lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}

NewReaderSize

NewReaderSize方法用于初始化操作,可以指定底层数据读取的 io.Reader 和 缓冲区的大小。默认缓冲区最小为 minReadBufferSize, 如果传入的size < minReadBufferSize,size 会被设置为 minReadBufferSize。

// 缓冲区的最小值
const minReadBufferSize = 16 

func NewReaderSize(rd io.Reader, size int) *Reader {
	
  // 如果传入的 rd 已经是 bufio.Reader,并且其缓冲区大小大于传入的size,那么 rd 就符合需求,直接返回 rd
	b, ok := rd.(*Reader)
	if ok && len(b.buf) >= size {
		return b
	}
  
  // 如果 size 参数小于默认的最小的缓冲区大小,size 置为 minReadBufferSize
	if size < minReadBufferSize {
		size = minReadBufferSize
	}
  
  // 初始化,然后调用 reset 方法赋值
	r := new(Reader)
	r.reset(make([]byte, size), rd)
	return r
}


// reset 根据传入的值,重置 bufio.Reader 的所有字段, r 和 w 会被置为 0 
func (b *Reader) reset(buf []byte, r io.Reader) {
	*b = Reader{
		buf:          buf,
		rd:           r,
		lastByte:     -1,
		lastRuneSize: -1,
	}
}

NewReader

NewReader方法 使用默认的缓冲区大小进行初始化,默认大小为 4k。

const (
	defaultBufSize = 4096
)

// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
	return NewReaderSize(rd, defaultBufSize)
}

Size

Size方法 返回缓冲切片的长度

// Size returns the size of the underlying buffer in bytes.
func (b *Reader) Size() int { return len(b.buf) }

Buffered

Buffered方法返回当前缓冲的字节数

func (b *Reader) Buffered() int { return b.w - b.r }

Reset

Reset 重置所有字段的状态,并将传入的 io.Reader r 作为底层新的数据读取器。重置所有状态,那么 r 和 w 也被重置为0,相当于将之前缓存的所有数据丢弃。

// Reset discards any buffered data, resets all state, and switches
// the buffered reader to read from r.
func (b *Reader) Reset(r io.Reader) {
  // 调用 私有方法 reset
	b.reset(b.buf, r)
}

reset

reset,私有方法 根据传入的值,重置自身所有字段, r 和 w 会被置为 0。 由于 r、w 被重置,相当于丢弃了所有缓存数据。

func (b *Reader) reset(buf []byte, r io.Reader) {
   *b = Reader{
      buf:          buf,
      rd:           r,
      lastByte:     -1,
      lastRuneSize: -1,
   }
}

fill

fill 私有方法 利用 io.Reader rd 将底层的数据读到缓冲区 buf 中。

  1. 方法首先会压缩缓存数组buf。如果已读计数 r>0,说明 r 之前有数据被读过,那么这些无效数据是可以丢弃的,而 b.r 和 b.w 之间的数据还没有被读取,是有意义的。因此利用数据平移的方式,将 b.buf[b.r:b.w] 这段数据移动到缓冲区最顶端,相当于整段数据向前移动b.r个位置,然后更新 r 和 w 的值。

有效数据大于等于无效数据长度

平移过程会有两种情况:有效数据长度大于等于无效数据,或者有效数据小于无效数据。上图属于第一种情况,平移过后会覆盖无效数据;对于第二种情况,有效数据不能完全覆盖当前的无效数据,但是因为我们划定有效数据的范围是根据 r 和 w 值,即b.buf[b.r:b.w],不在乎未覆盖的无效数据,在我们后续写入数据的过程中,这些无效数据就会被覆盖了。

有效数据小于无效数据长度

  1. 尝试从底层数据读取器 rd 中读取数据来填充缓冲区 buf。如果读取到数据或者产生 error,就会直接返回;但是如果底层数据还没准备好,既没有读取到数据,也没有产生 error,会重试读取,最多重试 100 次。
const maxConsecutiveEmptyReads = 100

// fill reads a new chunk into the buffer.
func (b *Reader) fill() {
	
  // 存在读取过的无效数据,数据平移,然后更新 r 和 w 的值
	if b.r > 0 {
		copy(b.buf, b.buf[b.r:b.w])
		b.w -= b.r
		b.r = 0
	}

	if b.w >= len(b.buf) {
		panic("bufio: tried to fill full buffer")
	}

	// 如果底层数据没有准备好,重试 maxConsecutiveEmptyReads 次
	for i := maxConsecutiveEmptyReads; i > 0; i-- {
    
    // rd 读取数据,从 w 位置开始写入到缓冲区
		n, err := b.rd.Read(b.buf[b.w:])
		if n < 0 {
			panic(errNegativeRead)
		}
    
    // 更新已写计数
		b.w += n
    
    // 如果产生了 error,赋值为 b.err,返回
		if err != nil {
			b.err = err
			return
		}
    
    // 没有产生 error,且读取到了数据,返回
		if n > 0 {
			return
		}
    
    // 到这里说明 err=nil,n=0,即底层无数据可读,进入重试阶段
	}
  
  // 重试 maxConsecutiveEmptyReads 次后都没有读到数据,设置 ErrNoProgress,然后返回 
	b.err = io.ErrNoProgress
}

readErr

readErr,私有方法,返回 b.err 的值,然后将 b.err 置为 nil。

func (b *Reader) readErr() error {
	err := b.err
	b.err = nil
	return err
}

总结

本篇文章我们介绍了 bufio.Reader 的基本结构和运行原理,并介绍了两个重要方法:

  • reset: 重置整个结构,相当于丢弃缓冲区的所有数据,同时将新的文件读取器作为 io.Reader rd
  • fill:首先压缩缓冲区的无效数据,然后尝试填充缓冲区

更多

个人博客: lifelmy.github.io/

微信公众号:漫漫Coding路