Go36-42,43-bufio包

bufio包

这是另一个与I/O操作强相关的代码包。bufio是“buffered I/O”的缩写,这个代码包中的程序实体实现的I/O操作都内置了缓冲区。

成都创新互联成立与2013年,先为青阳等服务建站,青阳等地企业,进行企业商务咨询服务。为青阳企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

主要数据类型

bufio包中的数据类型主要有:

  • Reader
  • Scanner
  • Writer和ReadWriter

与io包中的数据类型类似,这些类型的值也都需要在初始化的时候,包装一个或多个简单I/O接口类型的值。这里的简单I/O接口类型指的就是io包中的那些简单接口。

缓冲区的作用(bufio.Reader)

bufio.Reader类型的值内的缓冲区其实就是一个数据存储中介,它介于底层读取器与读取方法及其调用方之间。所谓的底层读取器,就是在初始化此类值的时候传入的io.Reader类型的参数值。
Reader值的读取方法一般会先从其所属值的缓冲区中读取数据。同时,在必要的时候,还会预先从底层读取器那里读出一部分数据,并暂存于缓冲区之中以备后用。有这样一个缓冲区的好处是,可以在大多数的时候降低读取方法的执行时间。虽然读取方法有时还要负责填充缓冲区,但从总体来看,读取方法的平均执行时间一般都会因此有大幅度的缩短。

bufio.Reader结构体中的字段

bufio.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
    lastRuneSize int
}

简要的解释一下结构体中的字段:

  1. buf,字节切片,代表缓冲区。虽然它是切片类型,但是其长度会在初始化的时候指定,并且之后保持不变。
  2. rd,代表底层读取器。缓冲区中的数据就是从这里拷贝出来的。
  3. r,代表对缓冲区进行下一次读取时的开始索引。可以称它为已读计数
  4. w,代表对缓冲区进行下一次写入是的开始缩写。可以称它为已写计数
  5. err,它的值表示在从底层读取器获得数据时发生的错误。这里的值在被读取或忽略之后,该字段会被置为nil。
  6. lastByte,记录缓冲区最后一个被读取的字节。读回退时会用到它的值。
  7. lastRuneSize,记录缓冲区最后一个被读取的Unicode字符所占用的字节数。读回退的时候会用到它的值。这个字段只会在其所属值的ReadRune方法中才会被赋予有意义的值。其他它情况都会被置为-1。

初始化函数

bufio包提供了两个用于用于初始化Reader值的函数,都会返回一个*bufio.Reader类型的值:

  • NewReader
  • NewReaderSze

NewReader函数初始化的Reader值会拥有一个默认尺寸的缓冲区。这个默认尺寸是4096个字节,即:4KB:

const (
    defaultBufSize = 4096
)

func NewReader(rd io.Reader) *Reader {
    return NewReaderSize(rd, defaultBufSize)
}

func NewReaderSize(rd io.Reader, size int) *Reader {
    // 内部代码省略
}

NewReaderSize函数则将缓冲区尺寸的决定权抛给了使用方。从上面的源码看,NewReader函数就是调用NewReaderSize的时候,指定了第二个用于决定缓冲区尺寸的参数。初始化函数的示例:

func main() {
    comment := "TEST"
    basicReader := strings.NewReader(comment)
    fmt.Println(basicReader.Size())
    reader1 := bufio.NewReader(basicReader)
    fmt.Println(reader1.Size())
    reader2 := bufio.NewReaderSize(basicReader, 128)
    fmt.Println(reader2.Size())
}

由于这里的缓冲区在一个Reader值的声明周期内其尺寸不可变,所以在有些时候是需要做一些权衡的。NewReaderSize函数就提供了这样一个途径。

填充缓冲区(fill方法)

在bufio.Reader类型拥有的读取方法中,Peek方法和ReadSlice方法都会调用该类型的一个名为fill的包级私有方法。fill方法的作用是填充内部缓冲区。
fill方法会先检查其所属值的已读计数。如果这个计数不大于0,那么有两种可能:

  • 缓冲区中的字节都是全新的,就是它们都没有被读取过
  • 缓冲区刚被压缩过

压缩
缓冲区的压缩包括两个步骤:

  1. 把缓冲区中在[已读计数, 已写计数)范围之内的所有元素值(或者说字节)都依次拷贝到缓冲区的头部
  2. 把已写计数的新值设定为原已写计数与原已读计数的差。这个差代表的索引,就是压缩后第一次写入字节时的开始索引

另外,fill方法还会把已读计数的值置为0,显然,在压缩之后,再读取字节就是从缓冲区的头部开始读了。
实际上,fill方法只要在开始时发现其所属值的已读计数大于0,就会对缓冲区进行一次压缩。之后,如果缓冲区还有可写的位置,那么该方法就会对其进行填充。

填充
在填充缓冲区的时候,fill方法会试图从底层读取器那里,读取足够多的字节,并尽量把从已写计数代表的索引位置到缓冲区末尾之间的空间都填满。在这个过程中,fill方法会及时的更新已写计数,以保证填充的正确性和顺序性。另外,它还会判断从底层读取器读取数据的时候,是否有错误发生。如果有,那么它就会把错误值赋给其所属值的err字段,并终止填充流程。

示例代码
下面是一个Peek方法使用的示例:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Hello, World!"
    basicReader := strings.NewReader(comment)
    fmt.Printf("字符串长度: %d\n", basicReader.Size())
    reader := bufio.NewReader(basicReader)
    fmt.Println("缓冲区长度:", reader.Size())
    // 此时缓冲区还没有被填充
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())

    bytes, err := reader.Peek(5)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
}

bufio.Writer

bufio.Writer类型有一个Flush方法,它的主要功能是把相应缓冲区中暂存的所有数据,都写到底层写入器中。数据一旦被写进底层写入器,该方法就会把这些数据从缓冲区中删除掉。这里的删除有时候只是逻辑上的删除而已。不论是否成功的写入了所有的暂存数据,Flush方法都会妥当处置,并保证不会出现重写和漏写的情况。该类型的字段n在此会起到很重要的作用。

bufio.Writer结构体中的字段

bufio.Writer结构体的定义如下:

type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

字段说明:

  1. err,用于表示在向底层写上器写数据时发生的错误。
  2. buf,代表缓冲区。在初始化之后,它的长度会保持不变。
  3. n,代表对缓冲区进行下一次写入时的开始索引。可以称之为已写计数
  4. wr,代表底层写入器。

Flush方法

bufio.Writer类型的值拥有的所有数据写入方法都会在必要的时候调用Flush方法。
比如,Write方法有时候会在把数据写进缓冲区之后,调用Flush方法,以便为后续的新数据腾出空间。WriteString方法的行为与之类似。
又比如,WriteByte方法和WriteRune方法,都会在发现缓冲区的可写空间不足以容纳新的字节或Unicode字符的时候,调用Flush方法。
此外,如果Write方法发现需要写入的字节太多,同时缓冲区已空,那么它就会跨过缓冲区,并直接把这些数据写到底层写入器中。
而ReadFrom,则会在发现底层写入器的类型是io.ReaderFrom接口的实现之后,直接调用其ReadFrom方法把参数值持有的数据写进去。
下面是一些示例代码:

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "strings"
)

func main() {
    comment := "Go is an open source programming language that makes it easy to build simple, " +
        "reliable,  " +
        "and efficient software."
    fmt.Println("全部的字节数:", len(comment))  // 112
    basicWriter1 := &strings.Builder{}
    size := 64
    writer1 := bufio.NewWriterSize(basicWriter1, size)
    fmt.Println("缓冲区大小:", size)
    fmt.Println()

    // WriteString方法调用Flush后,腾出空间
    start, end := 0, 41
    fmt.Println("写入字节数:", end-start)
    writer1.WriteString(comment[start:end])
    fmt.Println("缓冲区使用字节数:", writer1.Buffered())
    fmt.Println("缓冲区可用字节数:", writer1.Available())
    fmt.Println("Flush方法刷新缓冲区...")
    writer1.Flush()
    fmt.Println("缓冲区使用字节数:", writer1.Buffered())
    fmt.Println("缓冲区可用字节数:", writer1.Available())
    fmt.Println()

    // 写入的字节太多,
    start, end = 0, len(comment)  // 全部读完,所有的字节数大于缓冲区的大小
    fmt.Println("写入字节数:", end-start)
    writer1.WriteString(comment[start:end])
    fmt.Println("缓冲区使用字节数:", writer1.Buffered())
    fmt.Println("缓冲区可用字节数:", writer1.Available())
    fmt.Println("Flush方法刷新缓冲区...")
    writer1.Flush()
    fmt.Println()

    // ReadFrom会走捷径,不使用缓冲区
    basicWriter2 := &bytes.Buffer{}
    writer1.Reset(basicWriter2)
    reader := strings.NewReader(comment)
    writer1.ReadFrom(reader)
    fmt.Println("缓冲区使用字节数:", writer1.Buffered())
    fmt.Println("缓冲区可用字节数:", writer1.Available())
}

总之,在通常情况下,只要缓冲区中的可写空间无法容纳需要写入的新数据,Flush方法就一定会被调用。并且,bufio.Writer类型的一些方法有时候还会试图走捷径,跨过缓冲区而直接对接数据供需的双方。可以在理解了这些内部机制之后,明确的在代码里使用Flush方法。不过,也可以在把所有的数据都写入Writer值之后,再调用一下它的Flush方法,这是最稳妥的做法。

读取方法

bufio.Reader类型拥有很多用于读取数据的指针方法,其中有4个方法可以作为不同读取流程的代表:

  • Peek
  • Read
  • ReadSlice
  • ReadBytes

Peek方法

Peek方法的功能是:读取并返回其缓冲区中的n个未读字节,并且它会从已读计数代表的索引位置开始读。Peek方法还有一个特点。就是即使它读取了缓冲区中的数据,也不会更改已读计数。
在缓冲区未被填满,并且其中的未读字节的数量小于n的时候,该方法就会调用fill方法,以启动缓冲区填充流程。但是,如果发现上次填充缓冲区的时候有错误,就不会再次填充了。
Peek方法的使用示例:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Go is an open source programming language that makes it easy to build simple, " +
        "reliable,  " +
        "and efficient software."
    basicReader := strings.NewReader(comment)
    fmt.Println("字符串长度:", basicReader.Size())
    size := 64
    reader := bufio.NewReaderSize(basicReader, size)
    fmt.Println("缓冲区长度:", reader.Size())
    // 此时缓冲区还没有被填充
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
    fmt.Println()

    peekNum := 41
    bytes, err := reader.Peek(peekNum)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
    fmt.Println()

    // Peek方法不改变已读计数
    // 把上面用Peek方法读取的过程封装一下,反复调用
    peek(reader, 2)
    peek(reader, 5)
    peek(reader, 8)
}

func peek(reader *bufio.Reader, n int) {
    bytes, err := reader.Peek(n)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)
}

最开始,缓冲区为空,未读字节数量为0。调用Peek方法要读取41个字节。此时就会启动缓冲区填充流程。缓冲区会被填满,这里缓冲区的大小设定为64,也就是填满了64个字节。然后读取了41个字节。由于Peek方法不会改变已读计数,所以缓冲区里的所有内容都是未读的。所以,就算反复调用Peek方法,读到的内容也都是一样的。
如果调用方法给定的n比缓冲区的长度还要大,或者缓冲区中未读字节的数量小于n,那么Peek方法就会把所有未读字节返回,并且还会返回一个错误:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Hello, World!"
    basicReader := strings.NewReader(comment)

    // 缓冲区中未读字节数小于Peek方法指定的n
    reader1 := bufio.NewReader(basicReader)
    peekNum := len(comment) + 1
    bytes, err := reader1.Peek(peekNum)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("缓冲区中未读字节数: %d, Peek读取: %d\n", reader1.Buffered(), peekNum)
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)
    fmt.Println()

    // Peek方法指定的n比缓冲区长度还要大
    basicReader.Reset(comment)
    size := 300
    reader2 := bufio.NewReaderSize(basicReader, size)
    peekNum = size + 1
    bytes, err = reader2.Peek(peekNum)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("缓冲区长度: %d, Peek读取: %d\n", size, peekNum)
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)
}

这里两种读取错误的情况,都能正常返回读取的内容。不过同时,还会返回一非nil的错误值。

Read方法

Read方法,在缓冲区中还有未读字节的情况下,它会把缓冲区中的未读字节,依次拷贝到其参数p代表的字节切片中,并立即根据实际拷贝的字节数增加已读计数的值。
不过在另外一种情况下,其所属值的已读计数会等于已写计数,这说明缓冲区中已经没有任何未读的字节了。此时Read方法会先检查参数p的长度是否大于或等于缓冲区的长度。
如果缓冲区中已无未读字节,参数p的长度大于或等于缓冲区的长度。那么会放弃向缓冲区中填充数据,转而直接从起底层读取器读出数据并拷贝到p中。这意味着它完全跨如果缓冲区,并直连了数据供需的双方。
如果缓冲区中已无未读字节,缓冲区长度比参数p的长度更大。那么会先把已读计数和已写计数的值都重置为0,然后再尝试使用从底层读取器里获取的数据,对缓冲区进行一次从头至尾的填充。不过要注意,这里的尝试只会进行一次。无论在这一时刻是否能够获取到数据,也无论获取是是否有错误发生。而这与fill方法的做法不同,只要没有发生错误,fill方法就会进行多次尝试,因此fill方法真正获取到一些数据的可能性更大。所以Read方法中没有调用fill方法,而是有一段自己的代码实现缓冲区的填充。而这两个方法进行填充时的共同点是,只要把获取到的数据写入缓冲区,就会及时的更新已写计数的值。
Read方法的使用示例:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Hello, World!"
    basicReader := strings.NewReader(comment)
    fmt.Println("字符串长度:", basicReader.Size())
    reader := bufio.NewReader(basicReader)
    buf := make([]byte, 5)
    n, err := reader.Read(buf)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROE: %v\n", err)
    }
    fmt.Printf("Read读取(%d): %q\n", n, buf)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
}

ReadSlice方法

ReadSlice方法的功能是:持续的读取数据,直至遇到调用方给定的分隔符为止。
ReadSlice方法,会先在缓冲区的未读部分中寻找分隔符。如果未能找到,并且缓冲区未满,那么该方法会先通过调用fill方法对缓冲区进行填充,然后再次寻找。如果在填充过程中发生了错误(应该包括读到结尾了返回EOF错误),那么会把缓冲区中的未读部分作为结果返回,同时返回相应的错误值。
在上面的过程中,可能会出现虽然缓冲区已填满,但是仍然没能找到分隔符的情况。ReadSlice方法会把缓冲区里全部的内容返回,并返回缓冲区已满的错误。此时的缓冲区是经过fill方法填充的,肯定从头至尾都只包含未读的字节,所以这样做是合理的。
如果ReadSlice方法找到了分隔符,就会在缓冲区上切除相应的、包含分隔符的字节切片,并把该切片作为结果值返回。无论分隔符是否找到,该方法都会正确的设置已读计数的值。
ReadSlice方法的使用示例:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Go is an open source programming language that makes it easy to build simple, " +
        "reliable,  " +
        "and efficient software."
    basicReader := strings.NewReader(comment)
    reader := bufio.NewReader(basicReader)
    delimiter := byte(',')
    line, err := reader.ReadSlice(delimiter)
    if err != nil {
        fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err)
    }
    fmt.Printf("ReadSlice读取(%d): %q\n", len(line), line)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
    fmt.Println()

    delimiter = byte('!')  // 读不到这个分隔符
    line, err = reader.ReadSlice(delimiter)
    if err != nil {
        fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err)
    }
    fmt.Printf("ReadSlice读取(%d): %q\n", len(line), line)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
    fmt.Println()

    basicReader.Reset(comment)
    reader2 := bufio.NewReaderSize(basicReader, 80)
    delimiter = byte('!')  // 读不到这个分隔符
    line, err = reader2.ReadSlice(delimiter)
    if err != nil {
        fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err)
    }
    fmt.Printf("ReadSlice读取(%d): %q\n", len(line), line)
    fmt.Println("缓冲区里的未读字节数:", reader2.Buffered())
}

这个示例里也演示了,读完全部内容都没有找到分隔符,以及缓冲区已满并且其中没有包含分隔符这两种错误的情况。

ReadBytes方法

ReadBytes方法是基于ReadSlice方法实现的,它的内部会调用ReadSlice方法。
ReadSlice方法有一个问题,它是一个容易半途而废的方法。它可能会因为缓冲区已满而返回所有已读到的字节和相应的错误值,之后不会继续寻找。而ReadBytes方法就相当执着,它会通过调用ReadSlice方法一次又一次的从缓冲区中读取数据(源码里是一个无限for循环调用ReadSlice方法),直至找到分隔符为止。在这个过程中,ReadSlice方法可能会因为缓冲区已满而返回所有已读到的字节和响应的错误值,但ReadBytes方法会忽略掉这样的错误,并再次调用ReadSlice方法,这样就会继续填充缓冲区并寻找分隔符。除非ReadSlice方法返回的错误值不是缓冲区已满(errors.New("bufio: buffer full")),或者它找到了分隔符(返回错误值nil),否则这个过程就不会结束(因为在无限for循环中)。等到寻找过程结束,ReadBytes方法会把这个过程中读到的所有字节,都返回。如果过程结束是因为出现错误,那么第二个参数的错误值也会有内容返回。
ReadBytes方法的使用示例:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Go is an open source programming language that makes it easy to build simple, " +
        "reliable,  " +
        "and efficient software."
    basicReader := strings.NewReader(comment)
    reader := bufio.NewReaderSize(basicReader, 32)
    delimiter := byte(',')
    line, err := reader.ReadBytes(delimiter)
    if err != nil {
        fmt.Fprintf(os.Stderr, "EEEOR: %v\n", err)
    }
    fmt.Printf("ReadSlice读取(%d): %q\n", len(line), line)
    fmt.Println("缓冲区里的未读字节数:", reader.Buffered())
}

另外,bufio.Reader类型的ReadString方法完全依赖于这里的ReadBytes方法。只是在返回值的时候做了一个简单的类型转换,转成了字符串类型。具体可以看源码:

func (b *Reader) ReadString(delim byte) (string, error) {
    bytes, err := b.ReadBytes(delim)
    return string(bytes), err
}

ReadLine方法

在bufio.Reader类型的众多读取方法中,依赖ReadSlice方法的除了ReadBytes方法,还有ReadLine方法。这个方法是非常常用的一个方法,不过在读取流程上并没有什么特别的地方。这里就略了。

内容泄露

最后还有一个安全性的问题。bufio.Reader类型的Peek方法、ReadSlice方法和ReadLine方法都有可能造成内容泄露。主要是因为返回值是直接基于缓冲区的字节切片。这个问题在bytes包里已经提过了:调用方可以通过这些方法返回的接口值访问到缓冲区的其他部分,甚至是修改缓冲区中的内容。
在简单演示下获取到后面的内容,获取之后直接就可以操作扩张后的字节切片把里面的内容修改掉:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    comment := "Test contents leak."
    basicReader := strings.NewReader(comment)
    reader := bufio.NewReaderSize(basicReader, 30)
    bytes, err := reader.Peek(5)
    if err != nil {
        fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
    }
    fmt.Printf("Peek读取(%d): %q\n", len(bytes), bytes)

    // 扩张返回的字节切片
    bytes = bytes[:cap(bytes)]
    fmt.Printf("利用内容泄露获取到了所有的内容: %q\n", bytes)
}

分享文章:Go36-42,43-bufio包
转载注明:http://hbruida.cn/article/ieisdh.html