标准库的 bufio 包实现了带缓冲的 I/O。它对 io.Reader 或 io.Writer 进行封装,并返回一个新的 io.Reader 或 io.Writer 对象。新的 I/O 对象在原有的 io.Reader 或 io.Writer 对象之上提供缓冲功能,从而减少系统调用次数,以提高 I/O 性能。
带缓冲的 Reader
bufio.Reader
类型实现了带缓冲的 io.Reader
。它在实现读取操作时,会尽量一次性地从底层 Reader 读取数据并保存到缓冲区中。之后用户从缓冲 Reader 中读取数据时,它会尝试先返回缓冲区中的数据,只有必要时才继续从底层 Reader 读取数据。bufio.Reader
的定义如下:
1 | type Reader struct { |
缓冲 Reader 管理相关函数
NewReader()
函数以默认缓冲区大小 4096 创建一个带缓冲的 Reader:
1 | func NewReader(rd io.Reader) *Reader { |
NewReaderSize()
函数在创建缓冲 Reader 时,可以指定缓冲区的大小,但是最小长度为 16。
1 | func NewReaderSize(rd io.Reader, size int) *Reader |
值得说明的是,Reader 所使用的缓冲区在创建时长度就已经设置为对应的缓冲区大小了,它是靠 Reader.r
和 Reader.w
来实现对缓冲区读、写位置的管理。
1 | func NewReaderSize(rd io.Reader, size int) *Reader { |
Reader.Reset()
方法可以重新设置新的底层 Reader,该方法会丢弃缓冲区中已有的数据:
1 | func NewReader(rd io.Reader) *Reader |
Reader.Size()
方法用于返回缓冲区的大小:
1 | func (b *Reader) Size() int { return len(b.buf) } |
Reader.Buffered()
方法返回该 Reader 底层缓冲区中尚未处理的字节数,即可以直接从缓冲区中读取的数据长度。
1 | func (b *Reader) Buffered() int { return b.w - b.r } |
Read
系列方法
Reader.Read()
方法用于从缓冲 Reader 中读取数据,它的代码实现如下:
1 | func (b *Reader) Read(p []byte) (n int, err error) { |
可以看到该 Reader.Read()
方法的核心逻辑是:
- 只有当缓冲区中没有数据时,才会从底层 Reader 中读取数据
- 如果需要的数据大于缓冲区大小,此时直接从底层 Reader 中读取数据到 p 中
- 否则一次性地从底层 Reader 中读取 len(buf) 长度的数据到缓冲区中
- 如果缓冲区中存在数据,则本次直接返回缓冲区中的数据
Reader.ReadByte()
方法会尝试读取一个字节,它的实现如下:
1 | // 缓冲 Reader 实现了 ByteReader 接口,每次读取一个字节 |
该函数的核心逻辑是:
- 如果底层缓冲区中有数据,则直接从缓冲区返回一个数据即可
- 如果缓冲区中没有数据,会尝试调用
b.fill()
尝试从底层 reader 中读取数据,直到遇到错误
Reader.fill()
非导出方法实现了从底层 Reader 中读取数据并保存到缓冲区中,它的实现如下:
1 | // 从底层 reader 中读取数据 |
Reader.ReadRune()
方法会尝试读一个 Rune,返回该 rune、该 rune 的字节长度以及错误信息:
1 | func (b *Reader) ReadRune() (r rune, size int, err error) |
- 如果无法读取到数据,该函数返回 0,0 以及错误信息
- 即使字节序列是无效编码,该函数也会消耗一个字节,并返回
unicode.ReplacementChar (U+FFFD)
,size 认为是 1,而错误信息则为 nil
Reader.ReadSlice()
会尝试一直读取,直到遇到 delim 字节或者遇到错误(对于该函数,底层缓冲区满了也是一种错误)。该函数返回所读取的所有内容(包括结尾的 delim 字节)以及错误信息:
1 | func (b *Reader) ReadSlice(delim byte) (line []byte, err error) |
- 如果在找到 delim 字节之前,就遇到了错误,该函数会返回 buffer 中所有的数据以及错误信息
- 如果缓冲区已经满了,但是还是没有遇到 delim 字节,ReadSlice 返回的错误是 ErrBufferFull
- 当且仅当返回的 line 不是以 delim 字节结尾时,err 才不为 nil
- 由于该函数是直接返回底层 buffer 的切片而不是副本,因此在下次 I/O 操作后,返回的切片数据可能就被覆盖掉了。为了避免这个问题,可以使用
Reader.ReadBytes
或者Reader.ReadString
方法 - 只要返回了数据,该接口都支持
UnreadByte
以回退上一个字节
Reader.ReadLine()
方法是一个低级别的行读取行数,一般不会用到该函数。它的原型如下:
1 | func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) |
- 该函数尝试读取一行,并返回该行内容(不包过结尾的结尾的
\n
或者\r\n
) - 如果因为缓冲区满而无法完整地读取改行,此时会先返回该行中已经读取到的数据部分并将 isPrefix 设置为 true,期待下次继续调用该函数已返回剩余的内容
- 如果返回的是最后一个分段(不管结尾是否已
/n
或者/r/n
),isPrefix 设置为 false
Reader.ReadBytes()
函数类似于 ReadSlice
函数,但是它是以副本形式返回所读取到的数据(而不是直接返回底层 buffer 的切片),因此该函数不会返回 ErrBufferFull
错误,因为它总是新申请内存来保存所读取到的数据,因此它会一直尝试读取,直到遇到 delim 字节或者遇到错误,而不需要考虑底层缓冲区满的问题。同样,当且仅当返回的数据不是以 delim 字节结尾时,err 才不为 nil。
1 | func (b *Reader) ReadBytes(delim byte) ([]byte, error) |
Reader.ReadString()
函数类似于 Reader.ReadBytes()
函数,但是它是以字符串的形式返回数据,因此也可以认为返回的是底层 buffer 数据的副本。同样,当且仅当返回的数据不是以 delim 字节结尾时,err 才不为 nil。
1 | func (b *Reader) ReadString(delim byte) (string, error) |
Unread
系列方法
缓冲 Reader 实现了 UnreadByte()
方法,可以回退上一次读取操作的最后一个字节:
1 | func (b *Reader) UnreadByte() error |
缓冲 Reader 实现了 UnreadRune()
方法,可以回退上一次 ReadRune
操作所读取的 rune:
1 | func (b *Reader) UnreadRune() error |
bufio.Reader
实现了 io.WriterTo
接口,因此提供了 WriteTo()
方法,其核心逻辑是:
1 | func (b *Reader) WriteTo(w io.Writer) (n int64, err error) |
- 首先将缓冲区中剩余的、未被读取的数据写入 w 中
- 如果底层 rd 实现了
io.WriterTo
接口,则直接调用其rd.WriteTo
即可 - 如果 w 实现了
io.ReaderFrom
接口,则直接调用w.ReadFrom
方法即可 - 否则一直尝试从底层 rd 中读取数据,并写入 w 中
需要注意,WriteTo
会使 Unread 系列函数失效。
其他方法
Reader.Peek()
方法用于 Peek n 个字节的数据,即获取 Reader 中接下来的 n 个字节数据,但不会影响 Reader 的状态
1 | func (b *Reader) Peek(n int) ([]byte, error) |
- 由于是直接返回底层 buffer 的切片而不是副本,因此下次读取 I/O 操作后,这个字节切片可能就失效了
- 如果返回的字节数小于 n,则错误信息一定非 nil。如果返回的字节数等于 n,则错误信息为 nil
- 如果是因为缓冲区满而导致返回的字节数小于 n,则错误值为 ErrBufferFull
- Peek 操作会使 Unread 系列函数失效
Reader.Discard()
方法丢弃 Reader 中的 n 个字节。它返回实际丢弃的字节数以及遇到的错误信息:
1 | func (b *Reader) Discard(n int) (discarded int, err error) |
- 如果返回值 为 n,则 err 为 nil。如果返回值小于 n,则 err 肯定不为 nil
- 如果
0 <= n <= Reader.Buffered()
,该函数肯定不会失败,因为它不需要从底层 Reader 中读取数据 - 只有需要从底层 Reader 中读取数据进行丢弃时,才有可能遇到错误
- Discard 方法会使 Unread 系列函数失效
带缓冲的 Writer
bufio.Writer
实现了带缓冲的 io.Writer
,它在实现写入操作时,首先尝试将数据写入缓冲 buffer 中,当缓冲区满了的时候会尝试将缓冲区数据写入底层 io.Writer
中。bufio.Writer
的定义如下:
1 | type Writer struct { |
缓冲 Writer 管理相关函数
NewWriter()
函数以默认缓冲区大小 4096 创建一个带缓冲的 Writer:
1 | func NewWriter(w io.Writer) *Writer { |
NewWriterSize()
函数以指定大小创建带缓冲的 Writer:
1 | func NewWriterSize(w io.Writer, size int) *Writer |
同样,Writer 所使用的缓冲区在创建时长度就已经设置为对应的缓冲区大小了,它是靠 Writer.n
来实现对写入位置的管理。
Writer.Reset()
方法可以重新设置新的底层 Writer,同时也会丢弃缓冲区中已有的数据、错误信息等:
1 | func (b *Writer) Reset(w io.Writer) |
Writer.Size()
方法返回整个缓冲区的大小:
1 | func (b *Writer) Size() int { return len(b.buf) } |
Writer.Buffered()
方法返回缓冲区中已经缓存的字节数:
1 | func (b *Writer) Buffered() int { return b.n } |
Writer.Available()
方法返回缓冲区中剩余可用的缓冲区长度,即当前缓冲区还可以继续缓存的字节数:
1 | func (b *Writer) Available() int { return len(b.buf) - b.n } |
Writer.AvailableBuffer()
方法以字节切片的形式返回当前缓冲剩余可用的缓冲区,返回的字节切片长度为 0,但是 capactity
为 Writer.Available()
:
1 | return b.buf[b.n:][:0] |
一种典型用法是通过 append
等函数修改返回的 buffer 后,立即调用 Writer
方法进行写入:
1 | func main() { |
Writer.Flush()
方法将缓冲的数据都写入底层 io 中,并返回错误信息:
- 如果写入时发生错误,此时将返回对应的错误信息。同时缓冲区中会保留剩余的未写入完成的数据
- 如果写入的数据长度小于缓冲区长度,会返回
io.ErrShortWrite
- 如果缓冲区中的数据都写入成功,且没有发生错误,返回 nil
1 | func (b *Writer) Flush() error |
Write 系列方法
Writer.Write()
方法用于实现将数据写入缓冲 Writer 中。它的代码实现如下:
1 | func (b *Writer) Write(p []byte) (nn int, err error) { |
可以看到,其核心逻辑是:
- 当要写入的数据长度大于缓冲区剩余空间时
- 如果缓冲区没有数据,直接写入底层 Writer,避免 copy 的开销
- 否则将数据写入剩余缓冲区空间中,再通过
Writer.Flush
一次性写入整个缓冲区数据
- 如果要写入的数据长度小于缓冲区空间时,直接保存到缓冲区中即可
Writer.WriteByte()
方法用于将单个字节写入缓冲 Write 中:
1 | func (b *Writer) WriteByte(c byte) error |
Writer.WriteRune()
方法用于将一个 rune 写入缓冲 Write 中:
1 | func (b *Writer) WriteRune(r rune) (size int, err error) |
Writer.WriteString()
方法用于将一个字符串写入底层 Writer 中,它的实现逻辑类似于 Writer.Write
,只不过当要写入的数据长度大于缓冲区长度,且当前缓冲区中又没有数据时,其会判断底层 Writer 是否实现 WriteString
方法,如果实现了则直接调用该方法进行写入。
1 | func (b *Writer) WriteString(s string) (int, error) |
bufio.Writer
实现了 io.ReaderFrom
接口,其 ReadFrom
方法的核心逻辑如下:
1 | func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) |
- 如果缓冲区中没有数据,且底层 Writer 实现了
io.ReaderFrom
,则直接调用底层 Writer 的ReadFrom
方法 - 否则会尝试从 r 中读取数据并保存到缓冲区中,每次缓冲区满了,则尝试一次 Flush 操作
带缓冲的 ReadWriter
bufio 包还提供了一个带缓冲区的 ReadWriter 类型,它是直接通过嵌入匿名类型来实现的:
1 | type ReadWriter struct { |
1 | func NewReadWriter(r *Reader, w *Writer) *ReadWriter { |
Scanner
bufio.Scanner
提供了一个方便的接口来读取数据,可以按行、按单词、或者自定义分隔符的方式来读取数据。每次调用 Scanner.Scan
方法,会返回输入源中的一段 token
,tokens 之间的分隔字节会被跳过。可以通过 Scanner.Split
方法来指定如何分割 token
,默认就是以 换行符
作为分隔字节。
Scanner 也是基于一段缓冲区来保存从底层 Reader 中读取的数据,并基于缓冲区中的数据进行分割。缓冲区的最大长度是有限的,最大不能超过 64k。
1 | type Scanner struct { |
NewScanner()
函数用于创建一个 Scanner 对象,其中参数 r 指定了底层 reader,即从该 reader 中读取数据,再按照指定的分隔方法来返回 token
。
1 | func NewScanner(r io.Reader) *Scanner |
Scanner.Scan()
方法从输入源中读取下一段 token,读取到的 token 可以通过 Scanner.Bytes()
或者 Scanner.Text()
获取。如果没有更多 tokens(有可能是遇到 EOF 或者其他错误),该函数返回 false,此时可以通过 Scanner.Err()
获取对应的错误(如果是因为 EOF 而终止,该函数返回 nil 错误)。如果存在 token,则返回 true。
1 | func (s *Scanner) Scan() bool |
Scan 函数的核心逻辑是:
- 对缓冲区中已有的数据进行分割处理,判断是否能获取 token
- 如果不能获取 token,则尝试从底层 reader 中继续读取数据并保存到缓冲区中,之后再重新进行分割处理
- 如果缓冲区满了,会扩大缓冲区,但缓冲区大小不能超过
s.maxTokenSize
Scanner.Split()
方法用于指定分割函数,分隔函数的原型为 SplitFunc
1 | // data 参数是待处理的数据,atEOF 表示是否还有更多数据 |
以下函数都是预定义的分隔函数:
1 | // 每次返回一字节 |
Scanner.Buffer()
方法可以设置 Scanner 所使用的缓冲区,以及 token 的最大运行长度。
1 | func (s *Scanner) Buffer(buf []byte, max int) |
小结
bufio 包提供了带缓冲的 Reader 和 Writer,以提高 I/O 性能。同时 bufio 包还提供了一个 Scanner 类型,可以方便地从 io.Reader
中按照字节、rune、单词或行的方式来返回数据。