声明:该系列文章是基于对@golangspec、go官方文档、《Go语言标准库》的学习汇总而成
bufio封装了io.Reader和io.Writer并且通过缓存来提高性能。
bufio.Writer
为什么需要buffer?
在没有buffer的情况下,我们的writer函数会调用系统调用来完成一次写入,而小量写入下,系统调用的时间花销远远大于写入本身的花销。因此,过多的小量写入会大大影响性能。
因此我们可以使用bufio来封装writer,在内部,每次的write会变成写入buffer而不是直接写入磁盘。下面是一个四个字节的buffer的示意,我们可以看到,通过使用buffer,我们将原先需要的9次write调用变为了两次,从而大大提高了性能
producer buffer destination (io.Writer)
a -----> a
b -----> ab
c -----> abc
d -----> abcd
e -----> e ------> abcd
f -----> ef abcd
g -----> efg abcd
h -----> efgh abcd
i -----> i ------> abcdefgh
使用bufio.Writer
我们可以通过调用bufio.NewWriter(io.Writer)*Writer
来创建有buffer的writer。
在内部,会默认使用4096字节作为buffer,如果希望修改buffer大小可以使用bufio.NewWriterSize(io.Writer,int)*Writer
。
什么时候实质写入(调用底层writer的Write)
当buffer存满或者手动调用Flush()
函数的时候,bufio.Writer会调用底层Write写入buffer数据。
如果不自行调用Flush的话,bufio.Writer的buffer中可能会有数据未写入。
什么时候会触发错误?对错误如何处理?
当因为Flush或者buffer已满而调用Write的时候可能会触发错误。一旦某次Write调用触发了错误,再次写入会不执行而直接返回该错误。
当写入大于buffer容量的数据时,bufio.Writer如何处理?
当我们要写入的数据量(或者填满了当前buffer后的剩余数据量)大于buffer容量时候,会直接调用底层Write函数而不是存入buffer
如何获知当前已使用的buffer容量?
可以使用bufio.Writer.Buffered()
获取
如何重用buffer避免不必要的内存分配?
我们可以使用bufio.Writer.Reset(io.Writer)
来重新设置底层writer进而实现重用buffer的目的。
值得注意的是,该函数调用会直接清空buffer,因此在调用之前应当使用Flush处理buffer中的剩余数据。
如何获知有多少剩余空间?
可以使用bufio.Writer.Available()
bufio是否实现了ReaderFrom优化?有什么注意事项?
虽然bufio实现了ReaderFrom方法来优化io.Copy
等函数的性能,但是仍然要使用Flush来写入剩余数据!
type Writer int
func (*Writer) Write(p []byte) (n int, err error) {
fmt.Printf("%q\n", p)
return len(p), nil
}
func main() {
s := strings.NewReader("onetwothree")
w := new(Writer)
bw := bufio.NewWriterSize(w, 3)
//bw.Flush()
io.Copy(bw,s)
}
/**
"one"
"two"
"thr"
??ee去哪里了?
**/
bufio.Reader
bufio.Reader可以提前读取一定的字节而不是直接读取指定字节,从而降低了读取操作的次数,进而提高了性能。
如果消费者逐个字符的读取十个字符,那么就需要调用10次Read。而如果使用4byte缓存的bufio.Reader,则总共只需要调用三次
初始化bufio.Reader
可以使用bufio.NewReader(io.Reader)*bufio.Reader
来初始化bufio.Reader,默认buffer大小是4096字节,如果需要自行调整大小,可以使用bufio.NewReaderSize(io.Reader,int)*bufio.Reader
如何读取数据?
-
直接调用Read函数
这是最原始的办法,值得注意的是,Read函数最多只会调用一次底层Read:如果buffer不为空,则不会调用底层Read直接返回buffer中的内容;如果buffer为空,那么调用一次底层Read存储到buffer之后再写入(尽可能写多);如果buffer为空,且传入的切片长度大于buffer长度,则直接在传入切片上调用read
-
ReadSlice函数
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
可以用来读取直到遇到delim为止。结果会返回读取到delim之前的数据+delim本身。如果在读取到delim之前发生错误,则返回buffer中已有的数据+错误(通常是io.EOF);如果读取到填满了buffer还没有delim,则返回buffer数据+io.ErrBufferFull。总而言之,当返回的切片最后一个字符不是delim的时候,err一定不会nil
值得注意的是,这里返回的[]byte是reader内部buffer的切片(没有复制),这意味着下次read操作就会重写返回值,因此一般来说更常用的是ReadBytes或者ReadString
-
ReadBytes函数
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
Readbytes看起来有着和ReadSlice同样的签名,事实上ReadBytes内部会调用ReadSlice。它们之间的区别在于,ReadBytes可以多次调用ReadSlice直到找到delim,因此ReadBytes不受buffer大小的影响,而且ReadBytes会返回新的切片因此不用担心被覆盖。
和ReadSlice一样的是,如果在读到delim之前遇到一个错误,ReadBytes也会返回目前已经读到的数据以及错误(通常是io.EOF)
-
ReadString函数
是ReadBytes的简单封装,实现上就是将ReadBytes结果转换成string
-
ReadLine函数
ReadLine() (line []byte, isPrefix bool, err error)
该函数其实内部调用了
ReadSlice('\n')
,但是不会返回最后的\n,可以看到和ReadSlice签名上有些许差别,其中新增的isPrefix。如果最后也没有找到\n(此时buffer已经满了),isPrefix为true。如果找到了\n,则isPrefix为false。ReadLine返回的line是buffer的切片,因此在下次调用read的时候数据就会无效。更尴尬的是,输入没有换行符时没有任何提示或者错误会提醒你。
可以看出,从使用上来说,ReadBytes(’\n’)`更容易实现想要的
如何实现Peek操作?(允许多次查看)
可以使用Peek
函数,函数定义为func (b *Reader) Peek(n int) ([]byte, error)
,可以查看指定字节的数据。
如果buffer没有满且数据不够n字节,则会尝试调用Read来获取更多的数据。如果指定的n比buffer本身更大,那么会返回现有数据+bufio.ErrBufferFull
错误。如果n比stream更大,也就是说收到io.EOF了还没有读取到n个字节,那么返回现有数据+io.EOF
注意,返回的是内部buffer的切片,因此在下次读之后数据也会无效的。
如何重用buffer?
跟writer一样,可以使用Reset
,函数定义为func (b *Reader) Reset(r io.Reader)
。注意buffer的数据会丢失
如何丢掉指定字节的数据?
可以使用Discard
,函数定义为func (b *Reader) Discard(n int) (discarded int, err error)
。饭返回的discarded表示实际丢掉的字节数,如果discarded小于n,那么err!=nil。
如果buffer内已有的数据大于等于n,那么Discard不会调用read
bufio.Reader是否实现了WriterTo优化?
bufio.Reader实现了WriterTo接口,会读取直到底层返回io.EOF
bufio.NewReaderSize(reader,4)可以创建4字节的buffer吗?
不能,最小是16字节
频繁出现的返回内部切片意味着什么?有什么值得注意的地方?
意味着1. 每次调用新的read之前都需要先处理原先的数据 2. 多goroutine下不可靠
bufio.Scanner
bufio.Scanner试图解决reader在实现”读取一行、读取一个单词“等看似简单的任务上的复杂性。其主要用于流中的数据切分,如果数据已经在内存中,可以使用bytes和strings包中的Split等方法,会更加简单且直观。
如何初始化Scanner?
我们可以使用func NewScanner(r io.Reader) *Scanner
来初始化一个Scanner,该Scanner默认行为是按行切割得到token,buffer初始大小是4096字节
如何读取指定的Token?
我们只需要调用Scan
然后用Text
或者Bytes
读取即可。一个常见的范式是这么调用的
for scanner.Scan(){
t:=scanner.Text()
fmt.Println(t)
}
if scanner.Err()!=nil{
fmt.Printf("%q",scanner.Err())
}
如何重新设置buffer起始大小?
可以使用func (s *Scanner) Buffer(buf []byte, max int)
,buf指定了起始大小,而max则影响了可扩展的大小。token最大值=max(len(buf),max)。由此可见,如果max<len(buf),那么不会扩展。当buffer已经扩展至最大而splitFunc仍然无法解析token,则Scan返回false,Err返回ErrTooLong
注意的是,Buffer调用必须在Scan调用之前完成,否则会返回错误。
如何修改默认的“按行分隔”行为?
可以调用Split
来重新设置,函数定义是func (s *Scanner) Split(split SplitFunc)
,而其中涉及到的SplitFunc的定义是type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
。
如何理解SplitFunc?
该函数可能比较难以理解,data是剩余未被处理的字节切片,atEOF标识是否Reader没有更多的数据提供。而返回值advance标识提取出token“消耗”了多少字节,token则标识应该返回给用户的token,err则是错误本身。我们不妨看下面的例子来加深理解
基于返回值,splitFunc可以有三种表现形式:
-
需要更多的数据
传入的data不足以解析出一个token(比方说按行分隔还没遇到行),通过
0,nil,nil
来通知。scanner之后会尝试读取更多的数据进来,如果内置的buffer已满,那么就会尝试将其容量翻倍(最大不能超过最开始指定的max)input:=strings.NewReader("abcdefgh") scanner:=bufio.NewScanner(input) buf:=make([]byte,2) scanner.Buffer(buf,bufio.MaxScanTokenSize) sf:=func(d []byte,ae bool)(int,[]byte,error){ fmt.Printf("%t\t%d\t%s\n",ae,len(d),d) return 0,nil,nil//一直要求输入 } scanner.Split(sf) scanner.Scan() // 结果 false 2 ab false 4 abcd false 8 abcdefgh true 8 abcdefgh
atEOF会在scanner遇到EOF或者其他错误的时候为true。atEOF可以用于处理“最后在buffer中”的数据,如我们按行分割,最后一个token后面可能没有\n,这个时候我们就可以结合atEOF来判断这种情况,事实上,ScanLines就是这么操作的。
当返回
0,nil,nil
来表示需要更多数据而scanner发现已经EOF或者触发了错误,那么Scan就会返回false(从而退出循环) -
发现了Token
如果我们在提供的data中已经找到了一个token,我们可以用
advance,data,nil
来通知。advance表示这次寻找消耗了多少字节,data表示返回的token。如果说我们要找单词,而data是" abc ",那么返回5,"abc",nil
-
错误
如果split函数返回了错误,那么scanner会停止执行(Scan返回false)。
但是有一个例外,即如果返回的是
ErrFinalToken
错误,那么当次Scan会返回true,之后的每一次调用都返回false,这个错误是用于标识发现了最后一个token的场景。值得注意的是,io.EOF和bufio.ErrFinalToken错误都不被scanner认为是错误,即ERR方法返回nil
为什么Scan会panic?
这是一个比较常见的错误,因为有些时候,使用者在splitFunc中不正确的假定了当atEOF为true的时候data不为空。 #8672被fix之前,这样的代码会导致Scan永远为true而无法跳出循环。而fixed后变为如果检测到有100次返回token是空字符串,则panic
我们不妨用代码来解释
func main() {
input := "foo|bar"
scanner := bufio.NewScanner(strings.NewReader(input))
split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if i := bytes.IndexByte(data, '|'); i >= 0 {
return i + 1, data[0:i], nil
}
//这里假设了如果atEOF为true,那么data中有最后一组数据并返回
if atEOF {
return len(data), data[:len(data)], nil
}
return 0, nil, nil
}
scanner.Split(split)
for scanner.Scan() {
//为了美观,没有打出中间的空字符串
if scanner.Text() != "" {
fmt.Println(scanner.Text())
}
}
}
/**
输出结果如下
foo
bar
panic: bufio.Scan: too many empty tokens without progressing
**/
在这里我们如果调试代码,会发现第三次调用Scan(此时已经没有数据)时候,data为空,代码没有对此检测,而是直接return len(data), data[:len(data)], nil
,即返回0,[],nil
,空字符串也是合法的token因此Scan不会返回false来退出循环,于是即便这里没有panic,也会无限循环。
那么怎么简单的解决这个问题呢?如果我们参考ScanLines就会发现,只需要在函数开头对此做个判断即可
if atEOF && len(data)==0 {
return 0,nil,nil
}
该方式会通知scanner不足以构成token,随后scanner检查发现已经EOF,从而顺利退出
bufio.ReadWriter
bufio.ReadWriter就是利用组合的方式将bufio.Reader和bufio.Writer组合在一起,可以使用bufio.NewReadWriter(r *Reader, w *Writer) *ReadWriter
来实现这一点