前言

本文翻译自 How to read and write with Golang bufio


在 Go 语言中,bufio 是一个用于缓冲 IO 的包。缓冲 IO 是一种用于在转发之前临时累积 IO 操作结果的技术。这项技术可以通过减少系统调用的数量来提升程序的速度,系统调用通常是缓慢的操作。在这篇文章中,我们将看看 bufio 为写入和读写操作提供的一些抽象。

使用 bufio 写入

使用 bufio 写入

通过 bufio, 我们可以使用 bufio.Writer 方法在写入 IO 之前累计到缓冲区中。在下面的示例中,我们展示了你可能遇到的三种情况:

  1. 缓冲区已满
  2. 写入后缓冲区有空间
  3. 写入大于缓冲区容量

1. 缓冲区已满

一旦缓冲区满了,就执行写操作。

2. 写入后缓冲区有空间

如果缓冲区在最后一次写入后仍有空间,它将不会尝试完成写入,直到 Flush() 方法明确要求这样做。

3. 写入大于缓冲区容量

如果写入大于缓冲区容量,缓冲区将会被跳过,因为已经不需要缓冲了。

package main
import (
  "fmt"
	"bufio"
)

// 使用 Writer 类型初始化缓冲 writer
type Writer int

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

func main() {
  // 声明一个缓冲 writer
  // 缓冲大小为4字节
  w := new(Writer)
  bw := bufio.NewWriterSize(w, 4)

  // Case 1: 写满缓冲区

  bw.Write([]byte{'1'})
  bw.Write([]byte{'2'})
  bw.Write([]byte{'3'})
  bw.Write([]byte{'4'}) // write - 缓冲区满

  // Case 2: 缓冲仍有空间
  bw.Write([]byte{'5'})
	err := bw.Flush() // 强制写入剩下的
	if err != nil {
    panic(err)
	}

  // Case 3: 写入超过缓冲区容量
  // 将会跳过缓冲直接写入
  bw.Write([]byte("12345"))
}

其他写入功能

重用

我们可以使用 reset() 方法为不同的 writer 重用相同的 bufio.NewWriterSize

writerOne := new(Writer)
bw := bufio.NewWriterSize(writerOne,2) 
writerTwo := new(Writer)
bw.Reset(writerTwo) 

检查可用空间

我们可以使用 Available() 方法检查可用空间。

使用 bufio 读取

使用 bufio 读取

bufio 允许我们使用 bufio.Reader 批量读取。 读取后,根据需要将数据从缓冲区释放到消费者。 在下面的示例中,我们将看到:

  1. Peek
  2. ReadSlice
  3. ReadLine
  4. ReadByte
  5. Scanner

1. Peek

Peek(窥视) 方法让我们可以查看缓冲区中的前 n 个字节(称为 peek 值),而无需消耗它们。 该方法以下列方式操作。

  1. 如果 peek 值小于缓冲区容量,则返回等于 peek 值的字符。
  2. 如果 peek 值大于缓冲区容量,则返回 bufio.ErrBufferFull。
  3. 如果 peek 值包含 EOF 并且小于缓冲区容量,则返回 EOF。

2. ReadSlice

ReadSlice 方法签名:

func (b *Reader) ReadSlice(delim byte) (line []byte, err error)

它返回包含分隔符的字符串切片。例如,如果输入为 1,2,3 并且我们将逗号作为分隔符,将会输出:

1,
2,
3

如果找不到分隔符,并且已达到 EOF,则返回 io.EOF。 如果未达到分隔符且 readSlice 已超出缓冲区容量,则返回 io.ErrBufferFull。

3. ReadLine

ReadLine 定义如下:

ReadLine() (line []byte, isPrefix bool, err error)

ReadLine 内部使用 ReadSlice。 但是,它会从返回的切片中删除换行符(\n\r\n)。

请注意,它的签名是不同的,因为它也返回 isPrefix 标志。 当未找到分隔符且内部缓冲区已满时,此标志返回 true。

Readline 不处理长于内部缓冲区的行。 我们可以多次调用它来完成读取。

4. ReadByte

ReadByte 签名如下:

func (b *Reader) ReadBytes(delim byte) ([]byte, error)

与 ReadSlice 类似,ReadBytes 返回在分隔符之前(包括分隔符)的切片。 事实上,ReadByte 在 ReadSlice 上工作,它充当底层的低级函数。 但是,ReadByte 可以调用多个 ReadSlice 实例来累积返回数据; 因此,绕过缓冲区大小限制。 此外,由于 ReadByte 返回一个新的字节切片,因此使用起来更安全,因为随后的读取操作不会覆盖数据。

5. Scanner

Scanner(扫描) 通过将数据流拆分为 token 来中断数据流。 扫描在 EOF、第一次 IO 错误或令牌太大而无法放入缓冲区时停止。 如果需要对错误处理进行更多控制,请使用 bufio.Reader。Scanner 具有以下签名:

func NewScanner(r io.Reader) *Scanner

这是用于将文本划分为标记的 split 函数,默认为 ScanLines; 但是,如果需要你可以更改它。

package main
import (
	"bufio"
	"fmt"
  "strconv"
  "strings"
)

const singleLine string = "I'd love to have some coffee right about now"  
const multiLine string = "Reading is my...\r\n favourite"

func main() {
  
  fmt.Println("Lenght of singleLine input is " + strconv.Itoa(len(singleLine)))
  str := strings.NewReader(singleLine)
  br := bufio.NewReaderSize(str, 25) 
  
  fmt.Println("\n---Peek---")
  // Peek - Case 1: 简单 peek 实现
  b, err := br.Peek(3)
  if err != nil {
		fmt.Println(err)
	}
  fmt.Printf("%q\n",b)  // 输出: "I'd"

  // Peek - Case 2: Peek 大于缓存大小
  b, err = br.Peek(30)
  if err != nil {
		fmt.Println(err)  // 输出: "bufio: buffer full"
	}

  // Peek - Case 3: 缓冲大小大于字符串
  br_large := bufio.NewReaderSize(str,50) 
  b, err = br_large.Peek(50)
  if err != nil {
		fmt.Println(err)  // 输出: EOF
	}

  // ReadSlice
  fmt.Println("\n---ReadSlice---")
  str = strings.NewReader(multiLine)
  r := bufio.NewReader(str)
  for {
    token, err := r.ReadSlice('.')
    if len(token) > 0 {
			fmt.Printf("Token (ReadSlice): %q\n", token)
		}
		if err != nil {
			break
		}
  }


  // ReadLine 
  fmt.Println("\n---ReadLine---")
  str = strings.NewReader(multiLine)
  r = bufio.NewReader(str)
  for {
    token, _ , err := r.ReadLine()
    if len(token) > 0 {
			fmt.Printf("Token (ReadLine): %q\n", token)
		}
		if err != nil {
			break
		}
  }

  // ReadBytes
  fmt.Println("\n---ReadBytes---")
  str = strings.NewReader(multiLine)
  r.Reset(str)
  for {
		token, err := r.ReadBytes('\n')
		fmt.Printf("Token (ReadBytes): %q\n", token)
		if err != nil {
			break
		}
	}
 

  // Scanner
  fmt.Println("\n---Scanner---")
  str = strings.NewReader(multiLine)
	scanner := bufio.NewScanner(str)
	for scanner.Scan() {
		fmt.Printf("Token (Scanner): %q\n", scanner.Text())
	}
}