bufio — 缓存IO
source link: https://aimuke.github.io/go/2020/06/18/go-bufio-reader/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
bufio
包实现了缓存IO。它包装了 io.Reader
和 io.Writer
对象,创建了另外的 Reader
和 Writer
对象,它们也实现了 io.Reader
和 io.Writer
接口,不过它们是有缓存的。该包同时为文本I/O提供了一些便利操作。
Reader 类型和方法
bufio.Reader
结构包装了一个 io.Reader
对象,提供缓存功能,同时实现了 io.Reader
接口。
Reader
结构没有任何导出的字段,结构定义如下:
1
2
3
4
5
6
7
8
9
10
type Reader struct {
buf []byte // 缓存
rd io.Reader // 底层的io.Reader
// r:从buf中读走的字节(偏移);w:buf中填充内容的偏移;
// w - r 是buf中可被读的长度(缓存数据的大小),也是Buffered()方法的返回值
r, w int
err error // 读过程中遇到的错误
lastByte int // 最后一次读到的字节(ReadByte/UnreadByte)
lastRuneSize int // 最后一次读到的Rune的大小 (ReadRune/UnreadRune)
}
bufio
包提供了两个实例化 bufio.Reader
对象的函数: NewReader
和 NewReaderSize
。其中,NewReader
函数是调用 NewReaderSize
函数实现的:
1
2
3
4
func NewReader(rd io.Reader) *Reader {
// 默认缓存大小:defaultBufSize=4096
return NewReaderSize(rd, defaultBufSize)
}
我们看一下 NewReaderSize
的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func NewReaderSize(rd io.Reader, size int) *Reader {
// 已经是bufio.Reader类型,且缓存大小不小于 size,则直接返回
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
// 缓存大小不会小于 minReadBufferSize (16字节)
if size < minReadBufferSize {
size = minReadBufferSize
}
// 构造一个bufio.Reader实例
return &Reader{
buf: make([]byte, size),
rd: rd,
lastByte: -1,
lastRuneSize: -1,
}
}
ReadSlice、ReadBytes、ReadString 和 ReadLine 方法
之所以将这几个方法放在一起,是因为他们有着类似的行为。事实上,后三个方法最终都是调用 ReadSlice
来实现的。所以,我们先来看看 ReadSlice
方法。(感觉这一段直接看源码较好)
ReadSlice
ReadSlice
方法签名如下:
1
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
ReadSlice
从输入中读取,直到遇到第一个界定符(delim)为止,返回一个指向缓存中字节的 slice
,在下次调用读操作(read)时,这些字节会无效。举例说明:
1
2
3
4
5
6
7
reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadSlice('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))
1
2
3
4
the line:http://studygolang.com.
the line:It is the home of gophers
It is the home of gophers
从结果可以看出,第一次 ReadSlice
的结果(line),在第二次调用读操作后,内容发生了变化。也就是说, ReadSlice
返回的 []byte
是指向 Reader
中的 buffer
,而不是 copy 一份返回。正因为 ReadSlice
返回的数据会被下次的 I/O 操作重写,因此许多的客户端会选择使用 ReadBytes
或者 ReadString
来代替。读者可以将上面代码中的 ReadSlice
改为 ReadBytes
或 ReadString
,看看结果有什么不同。
注意,这里的界定符可以是任意的字符,可以将上面代码中的’\n
‘改为’m
‘试试。同时,返回的结果是包含界定符本身的,上例中,输出结果有一空行就是’\n
‘本身(line携带一个’\n
’, printf
又追加了一个’\n
’)。
如果 ReadSlice
在找到界定符之前遇到了 error
,它就会返回缓存中所有的数据和错误本身(经常是 io.EOF
)。如果在找到界定符之前缓存已经满了, ReadSlice
会返回 bufio.ErrBufferFull
错误。当且仅当返回的结果(line)没有以界定符结束的时候, ReadSlice
返回 err != nil
,也就是说,如果 ReadSlice
返回的结果 line 不是以界定符 delim 结尾,那么返回的 err
也一定不等于 nil
(可能是bufio.ErrBufferFull或io.EOF)。 例子代码:
1
2
3
4
5
reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com"),16)
line, err := reader.ReadSlice('\n')
fmt.Printf("line:%s\terror:%s\n", line, err)
line, err = reader.ReadSlice('\n')
fmt.Printf("line:%s\terror:%s\n", line, err)
1
2
line:http://studygola error:bufio: buffer full
line:ng.com error:EOF
ReadBytes
方法签名如下:
1
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
该方法的参数和返回值类型与 ReadSlice
都一样。 ReadBytes
从输入中读取直到遇到界定符(delim)为止,返回的 slice
包含了从当前到界定符的内容 (包括界定符)。如果 ReadBytes
在遇到界定符之前就捕获到一个错误,它会返回遇到错误之前已经读取的数据,和这个捕获到的错误(经常是 io.EOF)。跟 ReadSlice
一样,如果 ReadBytes
返回的结果 line 不是以界定符 delim 结尾,那么返回的 err
也一定不等于 nil
(可能是bufio.ErrBufferFull 或 io.EOF)。
从这个说明可以看出, ReadBytes
和 ReadSlice
功能和用法都很像,那他们有什么不同呢?
在讲解 ReadSlice
时说到,它返回的 []byte
是指向 Reader
中的 buffer
,而不是 copy
一份返回,也正因为如此,通常我们会使用 ReadBytes
或 ReadString
。很显然, ReadBytes
返回的 []byte
不会是指向 Reader
中的 buffer
,通过查看源码可以证实这一点。
还是上面的例子,我们将 ReadSlice
改为 ReadBytes
:
1
2
3
4
5
6
7
reader := bufio.NewReader(strings.NewReader("http://studygolang.com. \nIt is the home of gophers"))
line, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
// 这里可以换上任意的 bufio 的 Read/Write 操作
n, _ := reader.ReadBytes('\n')
fmt.Printf("the line:%s\n", line)
fmt.Println(string(n))
1
2
3
4
5
the line:http://studygolang.com.
the line:http://studygolang.com.
It is the home of gophers
ReadString
看一下该方法的源码:
1
2
3
4
func (b *Reader) ReadString(delim byte) (line string, err error) {
bytes, err := b.ReadBytes(delim)
return string(bytes), err
}
它调用了 ReadBytes
方法,并将结果的 []byte
转为 string
类型。
ReadLine
1
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
ReadLine
是一个底层的原始行读取命令。许多调用者或许会使用 ReadBytes('\n')
或者 ReadString
(‘\n’) 来代替这个方法。
ReadLine
尝试返回单独的行,不包括行尾的换行符。如果一行大于缓存, isPrefix
会被设置为 true,同时返回该行的开始部分(等于缓存大小的部分)。该行剩余的部分就会在下次调用的时候返回。当下次调用返回该行剩余部分时, isPrefix
将会是 false
。跟 ReadSlice
一样,返回的 line
只是 buffer
的引用,在下次执行IO操作时, line
会无效。可以将 ReadSlice
中的例子该为 ReadLine
试试。
注意,返回值中,要么 line 不是 nil,要么 err 非 nil,两者不会同时非 nil。
ReadLine
返回的文本不会包含行结尾(”\r\n"
或者”\n
“)。如果输入中没有行尾标识符,不会返回任何指示或者错误。
从上面的讲解中,我们知道,读取一行,通常会选择 ReadBytes
或 ReadString
。不过,正常人的思维,应该用 ReadLine
,只是不明白为啥 ReadLine
的实现不是通过 ReadBytes
,然后清除掉行尾的 \n
(或 \r\n
),它现在的实现,用不好会出现意想不到的问题,比如丢数据。个人建议可以这么实现读取一行:
1
2
line, err := reader.ReadBytes('\n')
line = bytes.TrimRight(line, "\r\n")
这样既读取了一行,也去掉了行尾结束符(当然,如果你希望留下行尾结束符,只用ReadBytes即可)。
Peek 方法
从方法的名称可以猜到,该方法只是“窥探”一下 Reader
中没有读取的 n
个字节。好比栈数据结构中的取栈顶元素,但不出栈。
方法的签名如下:
1
func (b *Reader) Peek(n int) ([]byte, error)
同上面介绍的 ReadSlice
一样,返回的 []byte
只是 buffer
中的引用,在下次IO操作后会无效,可见该方法(以及 ReadSlice
这样的,返回 buffer
引用的方法)对多 goroutine
是不安全的,也就是在多并发环境下,不能依赖其结果。
我们通过例子来证明一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"bufio"
"fmt"
"strings"
"time"
)
func main() {
reader := bufio.NewReaderSize(strings.NewReader("http://studygolang.com.\t It is the home of gophers"), 14)
go Peek(reader)
go reader.ReadBytes('\t')
time.Sleep(1e8)
}
func Peek(reader *bufio.Reader) {
line, _ := reader.Peek(14)
fmt.Printf("%s\n", line)
// time.Sleep(1)
fmt.Printf("%s\n", line)
}
1
2
http://studygo
http://studygo
输出结果和预期的一致。然而,这是由于目前的 goroutine 调度方式导致的结果。如果我们将例子中注释掉的 time.Sleep(1)
取消注释(这样调度其他 goroutine 执行),再次运行,得到的结果为:
1
2
http://studygo
ng.com. It is
另外, Reader
的 Peek
方法如果返回的 []byte
长度小于 n
,这时返回的 err != nil
,用于解释为啥会小于 n
。如果 n
大于 reader 的 buffer 长度,err 会是 ErrBufferFull。
Reader
的其他方法都是实现了 io 包中的接口,它们的使用方法在io包中都有介绍,在此不赘述。
这些方法包括:
1
2
3
4
5
6
func (b *Reader) Read(p []byte) (n int, err error)
func (b *Reader) ReadByte() (c byte, err error)
func (b *Reader) ReadRune() (r rune, size int, err error)
func (b *Reader) UnreadByte() error
func (b *Reader) UnreadRune() error
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)
你应该知道它们都是哪个接口的方法吧。
# References
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK