GO标准库巡礼-bufio

声明:该系列文章是基于对@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来实现这一点

发布了31 篇原创文章 · 获赞 32 · 访问量 733

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104945840