bufio 包实现了缓存I/O
。它提供了bufio.Reader
和bufio.Writer
类型,其内部分别包装了io.Reader
和io.Writer
对象,同时分别实现了io.Reader
和io.Writer
接口。同时,该包为文本I/O
提供了一些便利操作。
bufio.Reader 类型
bufio 包提供了带缓存功能的 Reader,在从硬盘中读取字节前使用内存缓存,可以节省操作硬盘I/O
的时间。在一些情况下它也很有用,比如每次读一个字节,可以一次性的从硬盘中读取缓存大小的数据到内存缓存中,然后读取缓存中的字节,减少硬盘的磨损以及提升性能。
// bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口
type Reader struct {
buf []byte // 缓存
rd io.Reader // 底层的 io.Reader
// r:从 buf 中读取的字节(偏移);w:buf 中写入内容的偏移;
// w - r 是 buf 中可被读的长度(缓存数据的大小),也是 Buffered() 方法的返回值
r, w int
err error // 读过程中遇到的错误
lastByte int // 最后一次读到的字节值,-1 是无效值
lastRuneSize int // 最后一次读到的 Rune 的大小,只有 ReadRune 操作才会修改该值,-1 是无效值
}
实例化
bufio 包提供了两个实例化 Reader 对象的函数。其中,NewReader()
函数本质上是调用NewReaderSize()
函数。具体如下:
// 创建一个具有默认大小缓存、从 r 读取的 *Reader,默认缓存大小为 4096 字节
func NewReader(r io.Reader) *Reader
// 创建一个具有最少有 size 大小的缓存、从 r 读取的 *Reader
// 如果参数 r 已经是一个具有足够大缓存的 *Reader 类型值,会返回 r
func NewReaderSize(r io.Reader, size int) *Reader
查看NewReader()
和NewReaderSize()
函数源码:
const (
defaultBufSize = 4096
)
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
func NewReaderSize(rd io.Reader, size int) *Reader {
// 已经是 bufio.Reader 类型,且缓存大小不小于 size,则直接返回
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
// 缓存大小不会小于 minReadBufferSize (16字节)
if size < minReadBufferSize {
size = minReadBufferSize
}
r := new(Reader)
r.reset(make([]byte, size), rd)
return r
}
读操作方法
// 从底层输入流读取最多 len(p) 个字节数据写入到 p 中,返回读取的字节数 n 和遇到的任何错误 err
// 读取到输入流结尾,n 可能返回非 0,err 返回 nil 或者 io.EOF,但是下次调用肯定返回 (0, io.EOF)
func (b *Reader) Read(p []byte) (n int, err error)
// 读取并返回一个字节。如果没有可用的数据,会返回错误
func (b *Reader) ReadByte() (c byte, err error)
// 还原最近一次读取操作读出的最后一个字节,相当于让读偏移量 r 前移一个字节
// 连续两次 UnreadByte 操作,而中间没有任何读取操作,会返回错误
func (b *Reader) UnreadByte() error
// 读取一个 utf-8 编码的 unicode 码值,返回该码值、其编码长度和可能的错误
// 如果 utf-8 编码非法,读取位置只移动 1 字节,返回 U+FFFD,返回值 size 为 1 而 err 为 nil。如果没有可用的数据,会返回错误
func (b *Reader) ReadRune() (r rune, size int, err error)
// 还原前一次 ReadRune 操作读取的 unicode 码值,相当于让读偏移量 r 前移一个码值长度
// 需要注意 UnreadRune 方法调用前必须调用 ReadRune 方法(从这点看,UnreadRune 比 UnreadByte 严格很多)
func (b *Reader) UnreadRune() error
// 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的切片
// 如果 ReadBytes 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io.EOF)
// 当且仅当 ReadBytes 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
// 读取直到第一次遇到 delim 字节,返回一个包含已读取的数据和 delim 字节的字符串
// 如果 ReadString 方法在读取到 delim 之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是 io.EOF)
// 当且仅当 ReadString 方法返回的切片不以 delim 结尾时,会返回一个非 nil 的错误
func (b *Reader) ReadString(delim byte) (line string, err error)
☕️ 示例代码
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 打开文件,只读
file, err := os.Open("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 默认缓存区大小为 4096 字节
reader := bufio.NewReader(file)
// 读取最多 10 个字节数据到字节切片 b 中
b := make([]byte, 10)
n, err := reader.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, b)
// 读取一个字节, 如果读取不成功会返回 Error
c, err := reader.ReadByte()
if err != nil {
panic(err)
}
fmt.Printf("Read 1 byte: %c\n", c)
// 还原最近一次读取操作读出的最后一个字节
err = reader.UnreadByte()
if err != nil {
panic(err)
}
// 读取一个字符, 如果读取不成功会返回 Error
r, size, err := reader.ReadRune()
if err != nil {
panic(err)
}
fmt.Printf("Read 1 character, %d bytes:%c\n", size, r)
// 还原前一次 ReadRune 操作读取的字符
err = reader.UnreadRune()
if err != nil {
panic(err)
}
// 读取到分隔符,包含分隔符,返回字节切片
b, err = reader.ReadBytes('\n')
if err != nil {
panic(err)
}
fmt.Printf("Read bytes: %s", b)
// 读取到分隔符,包含分隔符,返回字符串
str, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
panic(err)
}
fmt.Printf("Read string: %s", str)
}
// test.txt 内容
// hello world!!!!
// 你好,世界!!!!
// 控制台输出:
// Read 10 bytes: hello worl
// Read 1 byte: d
// Read 1 character,1 bytes:d
// Read bytes: d!!!!
// Read string: 你好,世界!!!!
Peek 方法
// 返回输入流的下 n 个字节,而不会移动读偏移量 r。返回的 []byte 是 buf 的引用,所以只在下一次调用读取操作前合法
// 如果 Peek() 返回的切片长度比 n 小,它也会返会一个错误说明原因。如果 n 比缓存尺寸还大,返回的错误将是 ErrBufferFull
func (b *Reader) Peek(n int) ([]byte, error)
⭐️ 示例代码
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 打开文件,只读
file, err := os.Open("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 默认缓存区大小为 4096 字节
reader := bufio.NewReader(file)
// 读取下 5 个字节的数据,文件读偏移量不动,返回的字节切片是内部 buf 的引用
b, err := reader.Peek(5)
if err != nil {
panic(err)
}
fmt.Printf("Peeked at 5 bytes: %s\n", b)
// 读取下 5 个字节的数据写入字节切片 b 中,文件读偏移量同时移动
b = make([]byte, 5)
n, err := reader.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, b)
}
// test.txt 内容:
// hello world!!!!
// 你好,世界!!!!
// 控制台输出:
// Peeked at 5 bytes: hello
// Read 5 bytes: hello
其它方法
// 返回缓存中现有的可读取的字节数
func (b *Reader) Buffered() int
// 实现 io.WriteTo 接口。从底层输入流中读取数据,写入输出流 w 中,返回写入的字节数和遇到的任何错误
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)
// 丢弃缓存中的数据,清除任何错误,将 b 重设为其下层从 r 读取数据
func (b *Reader) Reset(r io.Reader)
✏️ 示例代码
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 打开文件,只读
file, err := os.Open("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 默认缓存区大小为 4096 字节
reader := bufio.NewReader(file)
// 读取最多 15 个字节数据到字节切片 b 中
b := make([]byte, 15)
n, err := reader.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes: %s\n", n, b)
// 返回缓存中现有的可读取的字节数
fmt.Printf("Unread %d bytes\n", reader.Buffered())
// 将缓存区还未读的数据写入到标准输出
num, err := reader.WriteTo(os.Stdout)
if err != nil {
panic(err)
}
fmt.Printf("\nWrite %d bytes\n", num)
// 丢弃缓存中的数据,清除任何错误,重置输入流
reader.Reset(os.Stdin)
}
// test.txt 内容:
// hello world!!!!
// 你好,世界!!!!
// 控制台输出:
// Read 15 bytes: hello world!!!!
// Unread 29 bytes
//
// 你好,世界!!!!
// Write 29 bytes
bufio.Writer 类型
bufio 包提供了带缓存功能的 Writer,在写字节到硬盘前使用内存缓存,可以节省操作硬盘I/O
的时间。在一些情况下它也很有用,比如每次写一个字节,可以把它们攒在内存缓存中,然后一次写入到硬盘中,减少硬盘的磨损以及提升性能。
// bufio.Writer 结构包装了一个 io.Writer 对象,提供缓存功能,同时实现了 io.Writer 接口
type Writer struct {
err error // 写过程中遇到的错误
buf []byte // 缓存
n int // 当前缓存中的字节数
wr io.Writer // 底层的 io.Writer 对象
}
实例化
bufio 包同样提供了两个实例化 Writer 对象的函数。其中,NewWriter()
函数本质上是调用NewWriterSize()
函数。具体如下:
// 创建一个具有默认大小缓存、写入 w 的 *Writer,默认缓存大小为 4096 字节
func NewWriter(w io.Writer) *Writer
// 创建一个具有最少有 size 尺寸的缓存、写入 w 的 *Writer
// 如果参数 w 已经是一个具有足够大缓存的 *Writer 类型值,会返回 w
func NewWriterSize(w io.Writer, size int) *Writer
查看NewWriter()
和NewWriterSize()
函数源码:
const (
defaultBufSize = 4096
)
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
func NewWriterSize(w io.Writer, size int) *Writer {
// 已经是 bufio.Writer 类型,且缓存大小不小于 size,则直接返回
b, ok := w.(*Writer)
if ok && len(b.buf) >= size {
return b
}
if size <= 0 {
size = defaultBufSize
}
return &Writer{
buf: make([]byte, size),
wr: w,
}
}
写操作方法
// 将 p 的内容写入缓存,返回写入的字节数。如果返回值 n < len(p),返回一个错误说明原因
func (b *Writer) Write(p []byte) (n int, err error)
// 将单个字节写入缓存
func (b *Writer) WriteByte(c byte) error
// 将一个 unicode 码值写入缓存,返回写入的字节数和可能的错误
func (b *Writer) WriteRune(r rune) (size int, err error)
// 将一个字符串写入缓存,返回写入的字节数。如果返回值 n < len(s),返回一个错误说明原因
func (b *Writer) WriteString(s string) (n int, err error)
// 缓存满时,写操作会自动调用 Flush 方法。该方法将缓存数据刷新到底层的 io.Writer 对象
// 在所有的写操作完成之后,应该调用 Flush 方法使得缓存都写入底层的 io.Writer 对象
func (b *Writer) Flush() error
示例代码
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Create("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 默认缓存区大小为 4096 字节
writer := bufio.NewWriter(file)
// 写字节切片到缓存
n, err := writer.Write([]byte{65, 66, 67})
if err != nil {
panic(err)
}
fmt.Printf("Bytes written: %d\n", n)
// 写单个字节到缓存
err = writer.WriteByte('!')
if err != nil {
panic(err)
}
fmt.Printf("Bytes written: 1\n")
// 写单个字符到缓存
n, err = writer.WriteRune('您')
if err != nil {
panic(err)
}
fmt.Printf("Bytes written: %d\n", n)
// 写字符串到缓存
n, err = writer.WriteString("Hello World")
if err != nil {
panic(err)
}
fmt.Printf("Bytes written: %d\n", n)
// 将缓存数据刷新到底层的 io.Writer 对象
writer.Flush()
}
// 文件 test.txt 内容:
// ABC!您Hello World
// 控制台输出:
// Bytes written: 3
// Bytes written: 1
// Bytes written: 3
// Bytes written: 12
其它方法
// 返回缓存中已使用的字节数
func (b *Writer) Buffered() int
// 返回缓存中还有多少字节未使用
func (b *Writer) Available() int
// 实现了 io.ReaderFrom 接口。从输入流 r 中读取数据,写入底层输出流中,返回读取的字节数和遇到的任何错误
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)
// 丢弃缓存中的数据,清除任何错误,将 b 重设为将其输出写入 w
func (b *Writer) Reset(w io.Writer)
✌ 示例代码
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
file, err := os.Create("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
// 默认缓存区大小为 4096 字节
writer := bufio.NewWriter(file)
// 写字符串到缓存
n, err := writer.WriteString("Hello World\n")
if err != nil {
panic(err)
}
fmt.Printf("Bytes written: %d\n", n)
// 检查缓存中的已使用的字节大小
unFlushedNum := writer.Buffered()
fmt.Printf("Bytes buffered: %d\n", unFlushedNum)
// 检查缓存中未使用的字节大小
availableNum := writer.Available()
if err != nil {
panic(err)
}
fmt.Printf("Available buffer: %d\n", availableNum)
// 从输入流 r 读取数据,写入文件 file 中
r := strings.NewReader("您好!!!")
num, err := writer.ReadFrom(r)
if err != nil {
panic(err)
}
fmt.Printf("Read %d bytes\n", num)
// 检查缓存中的已使用的字节大小
unFlushedNum = writer.Buffered()
fmt.Printf("Bytes buffered: %d\n", unFlushedNum)
// 检查缓存中未使用的字节大小
availableNum = writer.Available()
if err != nil {
panic(err)
}
fmt.Printf("Available buffer: %d\n", availableNum)
// 将缓存数据刷新到底层的 io.Writer 对象
writer.Flush()
// 丢弃缓存中的数据,清除任何错误,将 b 重设为将其输出写入 w
writer.Reset(writer)
}
// 文件 test.txt 内容:
// Hello World
// 您好!!!
// 控制台输出:
// Bytes written: 12
// Bytes buffered: 12
// Available buffer: 4084
// Read 15 bytes
// Bytes buffered: 27
// Available buffer: 4069
bufio.ReadWriter 类型
bufio 包提供ReadWriter
结构存储了bufio.Reader
和bufio.Writer
类型的指针(内嵌),同时实现了io.ReadWriter
接口。
type ReadWriter struct {
*Reader
*Writer
}
// 创建一个新的、将读写操作分派给 r 和 w 的 ReadWriter
func NewReadWriter(r *Reader, w *Writer) *ReadWriter
bufio.Scanner 类型
Scanner 是 bufio 包下的类型,在处理文件中以分隔符分隔的文本时很有用。通常我们使用换行符作为分隔符将文件内容分成多行。在 CSV 文件中,逗号一般作为分隔符。os.File
文件可以被包装成bufio.Scanner
,它就像一个缓存 Reader。我们会调用Scan()
方法去读取下一个分隔符,使用Text()
或者Bytes()
获取读取的数据。
分隔符可以不是一个简单的字节或者字符,有一个特殊的方法可以实现分隔符的功能,以及将指针移动多少,返回什么数据。如果没有定制的SplitFunc
提供,缺省的ScanLines()
会使用 newline 字符作为分隔符,其它的分隔函数还包括ScanRunes()
和ScanWords()
,皆在 bufio 包中。
// SplitFunc 类型代表用于对输出作词法分析的分割函数
// 参数 data 是尚未处理的数据的一个开始部分的切片,参数 atEOF 表示是否 Reader 接口不能提供更多的数据。
// 返回值是解析位置前进的字节数,将要返回给调用者的 token 切片,以及可能遇到的错误。如果数据不足以(保证)
// 生成一个完整的 token,例如需要一整行数据但 data 里没有换行符,SplitFunc 可以返回(0, nil, nil)来告
// 诉 Scanner 读取更多的数据写入切片然后用从同一位置起始、长度更长的切片再试一次(调用 SplitFunc 类型函数)。
// 如果返回值 err 非 nil,扫描将终止并将该错误返回给 Scanner 的调用者。
// 除非 atEOF 为真,永远不会使用空切片 data 调用 SplitFunc 类型函数。然而,如果 atEOF 为真,data 却
// 可能是非空的、且包含着未处理的文本。
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
// 下面是 bufio 包定义的用于 Scanner 类型的分割函数(符合 SplitFunc)
// 本函数会将每个字节作为一个 token 返回
func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)
// 本函数会将每个 utf-8 编码的 unicode 码值作为一个 token 返回。
// 本函数返回的 rune 序列和 range 一个字符串的输出 rune 序列相同。错误的 utf-8 编码会翻译为 U+FFFD =
// "\xef\xbf\xbd",但只会消耗一个字节。调用者无法区分正确编码的 rune 和错误编码的 rune。
func ScanRunes(data []byte, atEOF bool) (advance int, token []byte, err error)
// 本函数会将空白(参见unicode.IsSpace)分隔的片段(去掉前后空白后)作为一个 token 返回。本函数永远不会返回空字符串
func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error)
// 本函数会将每一行文本去掉末尾的换行标记作为一个 token 返回,返回的行可以是空字符串。
// 换行标记为一个可选的回车后跟一个必选的换行符。最后一行即使没有换行符也会作为一个 token 返回
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
// bufio.Scanner 类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
// 创建并返回一个从 r 读取数据的 Scanner,默认的分割函数是 ScanLines
func NewScanner(r io.Reader) *Scanner
// 设置该 Scanner 的分割函数。本方法必须在 Scan 之前调用
func (s *Scanner) Split(split SplitFunc)
// 获取当前位置生成的 token(该 token 可以通过 Bytes 或 Text 方法获得),并让 Scanner 的扫描位置移动到下一个 token。
// 当扫描因为抵达输入流结尾或者遇到错误而停止时,本方法会返回 false。在 Scan 方法返回 false 后,Err 方
// 法将返回扫描时遇到的任何错误;除非是 io.EOF,此时 Err 会返回 nil
func (s *Scanner) Scan() bool
// 返回最近一次 Scan 调用生成的 token。底层数组指向的数据可能会被下一次 Scan 的调用重写
func (s *Scanner) Bytes() []byte
// 返回最近一次 Scan 调用生成的 token,会申请创建一个字符串保存 token 并返回该字符串
func (s *Scanner) Text() string
// 返回 Scanner 遇到的第一个非 EOF 的错误
func (s *Scanner) Err() error
✍ 示例代码
分别使用bufio.Reader
和bufio.Scanner
对象读取文件中的数据,一次读取一行:
// 使用 bufio.Reader 对象的 ReadBytes() 或 ReadString() 方法读取文件中的数据,一次读取一行
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Create("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!")
// 将文件 offset 设置到文件开头
file.Seek(0, io.SeekStart)
// 使用 Reader.ReadString() 方法读取文件
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if len(str) != 0 {
fmt.Printf(str)
}
if err == io.EOF {
break
} else if err != nil {
panic(err)
}
}
}
// 控制台输出
// http://studygolang.com.
// It is the home of gophers.
// If you are studying golang, welcome you!
// 使用 bufio.Scanner 对象的 Scan() 和 Text()/Bytes() 方法读取文件中的数据,一次读取一行
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Create("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString("http://studygolang.com.\nIt is the home of gophers.\nIf you are studying golang, welcome you!")
// 将文件 offset 设置到文件开头
file.Seek(0, io.SeekStart)
// 使用 Scanner 对象读取文件
scanner := bufio.NewScanner(file)
// Scan(): 获取当前位置生成的 token,移动到下一个 token
for scanner.Scan() {
// Text()/Bytes():返回最近一次 Scan 调用生成的 token
fmt.Println(scanner.Text())
}
// Err():返回 Scanner 遇到的第一个非 EOF 的错误
if err := scanner.Err(); err != nil {
panic(err)
}
}
// 控制台输出:
// http://studygolang.com.
// It is the home of gophers.
// If you are studying golang, welcome you!
示例代码
使用bufio.Scanner
对象统计文本中有多少个单词(不排除重复):
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Create("./test.txt")
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString("hello World gophers")
// 将文件 offset 设置到文件开头
file.Seek(0, io.SeekStart)
scanner := bufio.NewScanner(file)
// 缺省的分隔函数是 bufio.ScanLines,我们这里使用 ScanWords
// 也可以定制一个 SplitFunc 类型的分隔函数
scanner.Split(bufio.ScanWords)
count := 0
// Scan(): 获取当前位置生成的 token,移动到下一个 token
for scanner.Scan() {
count++
// Text()/Bytes():返回最近一次 Scan 调用生成的 token
fmt.Println(scanner.Text())
}
// Err():返回 Scanner 遇到的第一个非 EOF 的错误
if err := scanner.Err(); err != nil {
panic(err)
}
fmt.Println(count)
}
// 控制台输出:
// hello
// World
// gophers
// 3