乐趣区

go-bufio-缓冲读写详解级实例

go在提供了 io 包的同时也提供了 bufio 包来实现有缓存的读写操作以提高读写性能。为什么 bufio 性能比 io 高呢?

缓冲读写

缓冲读

// 默认缓冲区大小
const (defaultBufSize = 4096)

// 最小缓冲区大小 自定义小于次阈值将会被覆盖
const minReadBufferSize = 16

// 使用默认缓冲区大小
bufio.NewReader(rd io.Reader)
// 使用自定义缓冲区大小
bufio.NewReaderSize(rd io.Reader, size int)

缓冲读 的大致过程如下,设定好缓冲区大小 buf_size 后,读取的字节数为rn,缓冲的字节数为bn

  1. 如果缓冲区为空,且 rn >= buf_size,则直接从文件读取,不启用缓冲。
  2. 如果缓冲区为空,且 rn < buf_size,则从文件读取 buf_size 字节的内容到缓冲区,程序再从缓冲区中读取 rn 字节的内容,此时缓冲区剩余 bn = buf_size - rn 字节。
  3. 如果缓冲区不为空,rn < bn,则从缓冲区读取 rn 字节的内容,不发生文件IO
  4. 如果缓冲区不为空,rn >= bn,则从缓冲区读取 bn 字节的内容,不发生文件 IO,缓冲区置为空,回归1/2 步骤。

缓冲读通过预读,可以在一定程度上减少文件 IO 次数,故提高性能。

代码演示:

package main

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

func main() {
    // 用 strings.Reader 模拟一个文件 IO 对象
    strReader := strings.NewReader("12345678901234567890123456789012345678901234567890")
    
    // go 的缓冲区最小为 16 byte,我们用最小值比较容易演示
    bufReader := bufio.NewReaderSize(strReader, 16)

    // bn = 0 但 rn >= buf_size 缓冲区不启用 发生文件 IO
    tmpStr := make([]byte, 16)
    n, _ := bufReader.Read(tmpStr)
    // bufReader buffered: 0, content: 1234567890123456
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])

    // bn = 0 rn < buf_size 缓冲区启用
    // 缓冲区从文件读取 buf_size 字节 发生文件 IO
    // 程序从缓冲区读取 rn 字节
    // 缓冲区剩余 bn = buf_size - rn 字节
    tmpStr = make([]byte, 15)
    n, _ = bufReader.Read(tmpStr)
    // bufReader buffered: 1, content: 789012345678901
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])

    // bn = 1 rn > bn 
    // 程序从缓冲区读取 bn 字节 缓冲区置空 不发生文件 IO
    // 注意这里只能读到一个字节
    tmpStr = make([]byte, 10)
    n, _ = bufReader.Read(tmpStr)
    // bufReader buffered: 0, content: 2
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])

    // bn = 0 rn < buf_size 启用缓冲读 发生文件 IO
    // 缓冲区从文件读取 buf_size 字节
    // 程序从缓冲区读取 rn 字节
    // 缓冲区剩余 bn = buf_size - rn 字节
    tmpStr = make([]byte, 10)
    n, _ = bufReader.Read(tmpStr)
    // bufReader buffered: 6, content: 3456789012
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])

    // bn = 6 rn <= bn
    // 则程序冲缓冲区读取 rn 字节 不发生文件 IO
    tmpStr = make([]byte, 3)
    n, _ = bufReader.Read(tmpStr)
    // bufReader buffered: 3, content: 345
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])
    
    // bn = 3 rn <= bn
    // 则程序冲缓冲区读取 rn 字节 不发生文件 IO
    tmpStr = make([]byte, 3)
    n, _ = bufReader.Read(tmpStr)
    // bufReader buffered: 0, content: 678
    fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])
}

要注意的是当缓冲区中有内容时,程序的此次读取都会从缓冲区读,而不会发生文件 IO。只有当缓冲区为空时,才会发生文件 IO。如果缓冲区的大小足够,则启用缓冲读,先将内容载入填满缓冲区,程序再从缓冲区中读取。如果缓冲区过小,则会直接从文件读取,而不使用缓冲读。

缓冲写

// 使用 defaultBufSize 大小
func NewWriter(w io.Writer)
// 如果 size <= 0 则使用 defaultBufSize
func NewWriterSize(w io.Writer, size int)

缓冲写 的大致过程如下,设定好缓冲区大小 buf_size 后,写入的字节数为wn,缓冲的字节数为bn

  1. 如果缓冲区为空,且 wn >= buf_size,则直接写入文件,不启用缓冲,发生文件 IO。
  2. 如果缓冲区为空,且 wn < buf_size,则程序将内容写入缓冲区,不发生文件 IO。
  3. 如果缓冲区不为空,wn + bn < buf_size,则程序将内容写入缓冲区,不发生文件 IO。
  4. 如果缓冲区不为空,wn + bn >= buf_size,则缓冲区将 buf_size 字节内容写入文件,缓冲区 wn + bn - buf_size 的剩余内容。

简单说就是要写入的内容先缓冲着,缓冲不下了则将缓冲区内容写入文件。

代码演示:

package main

import (
    "bufio"
    "fmt"
    "io"
    "strings"
)

// 自定义一个 io.Writer 对象
type StringWriter struct {
}

func (s StringWriter) Write(p []byte) (n int, err error) {fmt.Printf("io write: %s\n", p)
    return len(p), nil
}

func main() {strReader := strings.NewReader("12345678901234567890")
    bufReader := bufio.NewReader(strReader)

    // 自定义的 io.Writer 对象
    var stringWriter io.Writer
    stringWriter = StringWriter{}

    // 写缓冲大小为 6 为了更好的演示我们自定义了一个 io.Writer
    bufWriter := bufio.NewWriterSize(stringWriter, 6)

    tmpStr := make([]byte, 8)
    for true {rn, err := bufReader.Read(tmpStr)
        if nil != err && io.EOF == err {break}

        _, err = bufWriter.Write(tmpStr[:rn])
        fmt.Printf("\nread and write: %s\n", tmpStr[:rn])
        fmt.Printf("bufWriter buffered: %d, available: %d, size: %d\n", bufWriter.Buffered(), bufWriter.Available(), bufWriter.Size())
        fmt.Printf("----------------------\n")
    }

    // 缓冲区最后剩余的一些内容
    _ = bufWriter.Flush()}
go run main.go
// 没有发生写动作 '1234' 被缓冲
read and write: 1234
bufWriter buffered: 4, available: 2, size: 6
----------------------
io write: 123456
// '12345678' 缓冲区满 则会写 '123456',继续缓冲 '78'
read and write: 5678
bufWriter buffered: 2, available: 4, size: 6
----------------------
// 没有发生写动作 '789012' 被缓冲
read and write: 9012
bufWriter buffered: 6, available: 0, size: 6
----------------------
io write: 789012
// '7890123456' 缓冲区满 则会写 '789012',继续缓冲 '3456'
read and write: 3456
bufWriter buffered: 4, available: 2, size: 6
----------------------
io write: 345678
// '34567890' 缓冲区满 则会写 '345678',继续缓冲 '90'
read and write: 7890
bufWriter buffered: 2, available: 4, size: 6
----------------------
// 文件读取完毕,输出剩余的 '90'
io write: 90
退出移动版