go 库I/O bufio
之前也有博客记录之 goio包
bufio
bufio.Reader
类型的值(以下简称Reader
值)内的缓冲区,其实就是一个数据存储中介,它介于底层读取器与读取方法及其调用方之间。所谓的底层读取器,就是在初始化此类值的时候传入的io.Reader
类型的参数值。
Reader
值的读取方法一般都会先从其所属值的缓冲区中读取数据。同时,在必要的时候,它们还会预先从底层读取器那里读出一部分数据,并暂存于缓冲区之中以备后用。
bufio.Reader
类型并不是开箱即用的,因为它包含了一些需要显式初始化的字段。为了让你能在后面更好地理解它的读取方法的内部流程,我先在这里简要地解释一下这些字段,如下所示。
buf
:[]byte
类型的字段,即字节切片,代表缓冲区。虽然它是切片类型的,但是其长度却会在初始化的时候指定,并在之后保持不变。rd
:io.Reader
类型的字段,代表底层读取器。缓冲区中的数据就是从这里拷贝来的。r
:int
类型的字段,代表对缓冲区进行下一次读取时的开始索引。我们可以称它为已读计数。w
:int
类型的字段,代表对缓冲区进行下一次写入时的开始索引。我们可以称之为已写计数。err
:error
类型的字段。它的值用于表示在从底层读取器获得数据时发生的错误。这里的值在被读取或忽略之后,该字段会被置为nil
。lastByte
:int
类型的字段,用于记录缓冲区中最后一个被读取的字节。读回退时会用到它的值。lastRuneSize
:int
类型的字段,用于记录缓冲区中最后一个被读取的Unicode字符所占用的字节数。读回退的时候会用到它的值。这个字段只会在其所属值的ReadRune
方法中才会被赋予有意义的值。在其他情况下,它都会被置为-1
。
bufio
包为我们提供了两个用于初始化Reader
值的函数,分别叫:
-
NewReader
; -
NewReaderSize
;
它们都会返回一个*bufio.Reader
类型的值。
NewReader
函数初始化的Reader
值会拥有一个默认尺寸的缓冲区。这个默认尺寸是4096个字节,即:4 KB。而NewReaderSize
函数则将缓冲区尺寸的决定权抛给了使用方。
由于这里的缓冲区在一个Reader
值的生命周期内其尺寸不可变,所以在有些时候是需要做一些权衡的。NewReaderSize
函数就提供了这样一个途径。
在bufio.Reader
类型拥有的读取方法中,Peek
方法和ReadSlice
方法都会调用该类型一个名为fill
的包级私有方法。fill
方法的作用是填充内部缓冲区、
fill
方法只要在开始时发现其所属值的已读计数大于0
,就会对缓冲区进行一次压缩。之后,如果缓冲区中还有可写的位置,那么该方法就会对其进行填充。
Reader
bufio.Reader
对io.Reader
进行了包装,提供了缓冲区功能。定义如下:
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 // 最后一个读取的字节,用于UnreadByte操作
lastRuneSize int // 最后一个读取rune的大小,用于UnreadRune操作
}
创建
通过NewReader(rd io.Reader)
可以创建一个新的Reader:
func NewReader(rd io.Reader) *Reader {
// const defaultBufSize = 4096
// 默认缓冲区大小为4K
return NewReaderSize(rd, defaultBufSize)
}
可以看到,NewReader
实际上是调用了NewReaderSize
方法,NewReaderSize
会创建一个具有特定大小缓冲区的Reader:
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
// const minReadBufferSize = 16
// 缓冲区最小为16byte
if size < minReadBufferSize {
size = minReadBufferSize
}
r := new(Reader)
r.reset(make([]byte, size), rd)
return r
}
func (b *Reader) reset(buf []byte, r io.Reader) {
*b = Reader{
buf: buf,
rd: r,
lastByte: -1,
lastRuneSize: -1,
}
}
read
和读操作相关的方法有:
- func (b *Reader) Read(p []byte) (n int, err error)
- func (b *Reader) ReadByte() (byte, error)
- func (b *Reader) ReadRune() (r rune, size int, err error)
- func (b *Reader) UnreadByte() error
- func (b *Reader) UnreadRune() error
- func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
- func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
- func (b *Reader) ReadBytes(delim byte) ([]byte, error)
- func (b *Reader) ReadString(delim byte) (string, error)
其中Read
方法源码为:
func (b *Reader) Read(p []byte) (n int, err error) {
n = len(p)
if n == 0 {
return 0, b.readErr()
}
// 如果b.r == b.w,则当前缓冲区中无数据
if b.r == b.w {
if b.err != nil {
return 0, b.readErr()
}
// 如果p的大小大于等于缓冲区大小,则直接将数据读入p,然后返回
if len(p) >= len(b.buf) {
// Large read, empty buffer.
// Read directly into p to avoid copy.
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()
}
// 如果p的大小小于缓冲区大小,则先将数据读入缓冲区
b.fill() // buffer is empty
if b.r == b.w {
return 0, b.readErr()
}
}
// 将缓冲区中的数据尽可能的拷贝到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
}
ReadSlice(delim byte)
会读取数据直到遇到分隔符delim
。如果在遇到delim
之前出错了或者缓冲区满了,也会退出。
ReadLine()
会读取一行数据,同样,在遇到换行符之前,如果出错了或者缓冲区满了,也会退出。因此该方法并不能保证遇到换行符的时候返回,也就是说,读到的数据可能并不够一行。例如:
r := strings.NewReader("0123456789abcdefghijklmn\nopqrstuvwxyz")
br := bufio.NewReaderSize(r, 16)
line, isPrefix, err := br.ReadLine()
fmt.Println(string(line)) // 0123456789abcdef
fmt.Println(isPrefix) // true
fmt.Println(err) // <nil>
因此,如果想要按行读取数据,使用ReadBytes('\n')
或者ReadString('\n')
会是更好的选择。ReadString
实际上调用了ReadBytes
,只不过将数据转成了字符串而已。ReadBytes(delim byte)
会不断地读取数据,直到遇到分隔符delim
。例如:
r := strings.NewReader("0123456789abcdefghijklmn\nopqrstuvwxyz")
br := bufio.NewReaderSize(r, 16)
line, err := br.ReadBytes('\n')
fmt.Println(string(line)) // 0123456789abcdefghijklmn
fmt.Println(err) // <nil>
其它操作
- func (b *Reader) Buffered() int
- func (b *Reader) Reset(r io.Reader)
- func (b *Reader) Discard(n int) (discarded int, err error)
- func (b *Reader) Peek(n int) ([]byte, error)
- func (b *Reader) WriteTo(w io.Writer) (n int64, err error)
Buffered
返回当前缓冲区中的可用数据量:
func (b *Reader) Buffered() int { return b.w - b.r }
Reset
会重置数据源,之后的数据读取都会从新的数据源中来读:
func (b *Reader) Reset(r io.Reader) {
b.reset(b.buf, r)
}
Discard(n int)
会跳过之后的n
个字节,例如:
br := bufio.NewReader(strings.NewReader("0123456789"))
p := make([]byte, 5)
br.Discard(3)
br.Read(p)
fmt.Println(string(p)) // 34567
Peek(n int)
用于查看接下来的n
个字节数据,但是并不真正读取,例如:
br := bufio.NewReader(strings.NewReader("0123456789"))
p := make([]byte, 5)
br.Peek(3)
br.Read(p)
fmt.Println(string(p)) // 01234
WriteTo
将数据写入到一个Writer中,因此bufio.Reader
实现了io.WriterTo
接口。
bufio.Reader
类型读取方法有哪些不同?
bufio.Reader
类型拥有很多用于读取数据的指针方法,这里面有4个方法可以作为不同读取流程的代表,它们是:Peek
、Read
、ReadSlice
和ReadBytes
。
Reader
值的Peek
方法的功能是:读取并返回其缓冲区中的n
个未读字节,并且它会从已读计数代表的索引位置开始读。
在缓冲区未被填满,并且其中的未读字节的数量小于n
的时候,该方法就会调用fill
方法,以启动缓冲区填充流程。但是,如果它发现上次填充缓冲区的时候有错误,那就不会再次填充。
如果调用方给定的n
比缓冲区的长度还要大,或者缓冲区中未读字节的数量小于n
,那么Peek
方法就会把“所有未读字节组成的序列”作为第一个结果值返回。
同时,它通常还把“bufio.ErrBufferFull
变量的值(以下简称缓冲区已满的错误)”- 作为第二个结果值返回,用来表示:虽然缓冲区被压缩和填满了,但是仍然满足不了要求。
只有在上述的情况都没有出现时,Peek
方法才能返回:“以已读计数为起始的n
个字节”和“表示未发生任何错误的nil
”。
bufio.Reader
类型的Peek方法有一个鲜明的特点,那就是:即使它读取了缓冲区中的数据,也不会更改已读计数的值。
这个类型的其他读取方法并不是这样。就拿该类型的Read
方法来说,它有时会把缓冲区中的未读字节,依次拷贝到其参数p
代表的字节切片中,并立即根据实际拷贝的字节数增加已读计数的值。
-
在缓冲区中还有未读字节的情况下,该方法的做法就是如此。不过,在另一些时候,其所属值的已读计数会等于已写计数,这表明:此时的缓冲区中已经没有任何未读的字节了。
-
当缓冲区中已无未读字节时,
Read
方法会先检查参数p
的长度是否大于或等于缓冲区的长度。如果是,那么Read
方法会索性放弃向缓冲区中填充数据,转而直接从其底层读取器中读出数据并拷贝到p
中。这意味着它完全跨过了缓冲区,并直连了数据供需的双方。
再来说ReadSlice
方法和ReadBytes
方法。 这两个方法的功能总体上来说,都是持续地读取数据,直至遇到调用方给定的分隔符为止。
ReadSlice
方法会先在其缓冲区的未读部分中寻找分隔符。如果未能找到,并且缓冲区未满,那么该方法会先通过调用fill
方法对缓冲区进行填充,然后再次寻找,如此往复。
如果在填充的过程中发生了错误,那么它会把缓冲区中的未读部分作为结果返回,同时返回相应的错误值。
注意,在这个过程中有可能会出现虽然缓冲区已被填满,但仍然没能找到分隔符的情况。
这时,ReadSlice
方法会把整个缓冲区(也就是buf
字段代表的字节切片)作为第一个结果值,并把缓冲区已满的错误(即bufio.ErrBufferFull
变量的值)作为第二个结果值。
经过fill
方法填满的缓冲区肯定从头至尾都只包含了未读的字节,所以这样做是合理的。
当然了,一旦ReadSlice
方法找到了分隔符,它就会在缓冲区上切出相应的、包含分隔符的字节切片,并把该切片作为结果值返回。无论分隔符找到与否,该方法都会正确地设置已读计数的值。
比如,在返回缓冲区中的所有未读字节,或者代表全部缓冲区的字节切片之前,它会把已写计数的值赋给已读计数,以表明缓冲区中已无未读字节。
// ReadSlice reads until the first occurrence of delim in the input,
// returning a slice pointing at the bytes in the buffer.
// The bytes stop being valid at the next read.
// If ReadSlice encounters an error before finding a delimiter,
// it returns all the data in the buffer and the error itself (often io.EOF).
// ReadSlice fails with error ErrBufferFull if the buffer fills without a delim.
// Because the data returned from ReadSlice will be overwritten
// by the next I/O operation, most clients should use
// ReadBytes or ReadString instead.
// ReadSlice returns err != nil if and only if line does not end in delim.
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
s := 0 // search start index
for {
// Search buffer.
if i := bytes.IndexByte(b.buf[b.r+s:b.w], delim); i >= 0 {
i += s
line = b.buf[b.r : b.r+i+1]
b.r += i + 1
break
}
// Pending error?
if b.err != nil {
line = b.buf[b.r:b.w]
b.r = b.w
err = b.readErr()
break
}
// Buffer full?
if b.Buffered() >= len(b.buf) {
b.r = b.w
line = b.buf
err = ErrBufferFull
break
}
s = b.w - b.r // do not rescan area we scanned before
b.fill() // buffer is not full
}
// Handle last byte, if any.
if i := len(line) - 1; i >= 0 {
b.lastByte = int(line[i])
b.lastRuneSize = -1
}
return
}
ReadBytes
方法会通过调用ReadSlice
方法一次又一次地从缓冲区中读取数据,直至找到分隔符为止。
在这个过程中,ReadSlice
方法可能会因缓冲区已满而返回所有已读到的字节和相应的错误值,但ReadBytes
方法总是会忽略掉这样的错误,并再次调用ReadSlice
方法,这使得后者会继续填充缓冲区并在其中寻找分隔符。
除非ReadSlice
方法返回的错误值并不代表缓冲区已满的错误,或者它找到了分隔符,否则这一过程永远不会结束。
如果寻找的过程结束了,不管是不是因为找到了分隔符,ReadBytes
方法都会把在这个过程中读到的所有字节,按照读取的先后顺序组装成一个字节切片,并把它作为第一个结果值。如果过程结束是因为出现错误,那么它还会把拿到的错误值作为第二个结果值。
在bufio.Reader
类型的众多读取方法中,依赖ReadSlice
方法的除了ReadBytes
方法,还有ReadLine
方法。
bufio.Reader
类型的Peek
方法、ReadSlice
方法和ReadLine
方法都有可能会造成内容泄露。---->调用方可以通过这些方法返回的结果值访问到缓冲区的其他部分,甚至修改缓冲区中的内容
在Reader
值拥有的众多读取方法中,有4个方法可以作为不同读取流程的代表,它们是:Peek
、Read
、ReadSlice
和ReadBytes
。
Peek
方法的特点是即使读取了缓冲区中的数据,也不会更改已读计数的值。而Read
方法会在参数值的长度过大,且缓冲区中已无未读字节时,跨过缓冲区并直接向底层读取器索要数据。
ReadSlice
方法会在缓冲区的未读部分中寻找给定的分隔符,并在必要时对缓冲区进行填充。
如果在填满缓冲区之后仍然未能找到分隔符,那么该方法就会把整个缓冲区作为第一个结果值返回,同时返回缓冲区已满的错误。
ReadBytes
方法会通过调用ReadSlice
方法,一次又一次地填充缓冲区,并在其中寻找分隔符。除非发生了未预料到的错误或者找到了分隔符,否则这一过程将会一直进行下去。
Reader
值的ReadLine
方法会依赖于它的ReadSlice
方法,而其ReadString
方法则完全依赖于ReadBytes
方法。
另外,值得我们特别注意的是,Reader
值的Peek
方法、ReadSlice
方法和ReadLine
方法都可能会造成其缓冲区中的内容的泄露。
最后再说一下bufio.Writer
类型。把该类值的缓冲区中暂存的数据写进其底层写入器的功能,主要是由它的Flush
方法实现的。
Writer
bufio.Writer
对io.Writer
进行了包装,提供了缓冲区功能。定义如下:
type Writer struct {
err error
buf []byte // 缓冲区
n int // 缓冲区的可用数据量
wr io.Writer
}
通过如下方法可以创建新的Writer:
- func NewWriter(w io.Writer) *Writer
- func NewWriterSize(w io.Writer, size int) *Writer
write
写操作相关方法有:
- func (b *Writer) Write(p []byte) (nn int, err error)
- func (b *Writer) WriteByte(c byte) error
- func (b *Writer) WriteRune(r rune) (size int, err error)
- func (b *Writer) WriteString(s string) (int, error)
其中,Write
方法源码为:
func (b *Writer) Write(p []byte) (nn int, err error) {
// b.Available() 的值为 len(b.buf) - b.n
// 只要p的大小大于缓冲区的可用大小,则执行循环
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
// 如果p的大小大于缓冲区的可用大小,且缓冲区为空
// 则数据直接写入,无需先拷贝到缓冲区
n, b.err = b.wr.Write(p)
} else {
// 将数据拷贝到缓冲区,然后通过flush操作写入缓冲区数据
n = copy(b.buf[b.n:], p)
b.n += n
b.flush()
}
nn += n
// 剩余待写入数据
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
// 此时p的大小小于等于缓冲区大小,因此将数据拷贝到缓冲区
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
问题1:bufio.Writer
类型值中缓冲的数据什么时候会被写到它的底层写入器?
我们先来看一下bufio.Writer
类型都有哪些字段:
err
:error
类型的字段。它的值用于表示在向底层写入器写数据时发生的错误。buf
:[]byte
类型的字段,代表缓冲区。在初始化之后,它的长度会保持不变。n
:int
类型的字段,代表对缓冲区进行下一次写入时的开始索引。我们可以称之为已写计数。wr
:io.Writer
类型的字段,代表底层写入器。
bufio.Writer
类型有一个名为Flush
的方法,它的主要功能是把相应缓冲区中暂存的所有数据,都写到底层写入器中。数据一旦被写进底层写入器,该方法就会把它们从缓冲区中删除掉。
不过,这里的删除有时候只是逻辑上的删除而已。不论是否成功地写入了所有的暂存数据,Flush
方法都会妥当处置,并保证不会出现重写和漏写的情况。该类型的字段n
在此会起到很重要的作用。
bufio.Writer
类型值(以下简称Writer
值)拥有的所有数据写入方法都会在必要的时候调用它的Flush
方法。
比如,Write
方法有时候会在把数据写进缓冲区之后,调用Flush
方法,以便为后续的新数据腾出空间。WriteString
方法的行为与之类似。
又比如,WriteByte
方法和WriteRune
方法,都会在发现缓冲区中的可写空间不足以容纳新的字节,或Unicode字符的时候,调用Flush
方法。
此外,如果Write
方法发现需要写入的字节太多,同时缓冲区已空,那么它就会跨过缓冲区,并直接把这些数据写到底层写入器中。
而ReadFrom
方法,则会在发现底层写入器的类型是io.ReaderFrom
接口的实现之后,直接调用其ReadFrom
方法把参数值持有的数据写进去。
总之,在通常情况下,只要缓冲区中的可写空间无法容纳需要写入的新数据,Flush
方法就一定会被调用。并且,bufio.Writer
类型的一些方法有时候还会试图走捷径,跨过缓冲区而直接对接数据供需的双方。
你可以在理解了这些内部机制之后,有的放矢地编写你的代码。不过,在你把所有的数据都写入Writer
值之后,再调用一下它的Flush
方法,显然是最稳妥的。
其它操作
- func (b *Writer) Available() int
- func (b *Writer) Buffered() int
- func (b *Writer) Flush() error
- func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)
- func (b *Writer) Reset(w io.Writer)
Available()
返回的是缓冲区中的可用大小:
func (b *Writer) Available() int { return len(b.buf) - b.n }
Buffered()
返回的是缓冲区中已经缓存的数据大小:
func (b *Writer) Buffered() int { return b.n }
ReadWriter
bufio.ReadWriter
实现了io.ReadWriter
接口,同时包含了一个Reader和一个Writer:
type ReadWriter struct {
*Reader
*Writer
}
func NewReadWriter(r *Reader, w *Writer) *ReadWriter {
return &ReadWriter{r, w}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2022-01-24 protobuf 到json的互转