43 | bufio包中的数据类型(下)

在上一篇文章中,我提到了bufio包中的数据类型主要有Reader、Scanner、Writer和ReadWriter。并着重讲到了bufio.Reader类型与bufio.Writer类型,今天,我们继续专注bufio.Reader的内容来进行学习。

知识扩展

问题 :bufio.Reader类型读取方法有哪些不同?

bufio.Reader类型拥有很多用于读取数据的指针方法,这里面有 4 个方法可以作为不同读取流程的代表,它们是:Peek、Read、ReadSlice和ReadBytes。

Reader值的Peek方法的功能是:读取并返回其缓冲区中的n个未读字节,并且它会从已读计数代表的索引位置开始读。

在缓冲区未被填满,并且其中的未读字节的数量小于n的时候,该方法就会调用fill方法,以启动缓冲区填充流程。但是,如果它发现上次填充缓冲区的时候有错误,那就不会再次填充。

如果调用方给定的n比缓冲区的长度还要大,或者缓冲区中未读字节的数量小于n,那么Peek方法就会把“所有未读字节组成的序列”作为第一个结果值返回。

同时,它通常还把“bufio.ErrBufferFull变量的值(以下简称缓冲区已满的错误)”

作为第二个结果值返回,用来表示:虽然缓冲区被压缩和填满了,但是仍然满足不了要求。

只有在上述的情况都没有出现时,Peek方法才能返回:“以已读计数为起始的n个字节”和“表示未发生任何错误的nil”。

bufio.Reader类型的 Peek 方法有一个鲜明的特点,那就是:即使它读取了缓冲区中的数据,也不会更改已读计数的值。

这个类型的其他读取方法并不是这样。就拿该类型的Read方法来说,它有时会把缓冲区中的未读字节,依次拷贝到其参数p代表的字节切片中,并立即根据实际拷贝的字节数增加已读计数的值。

  • 在缓冲区中还有未读字节的情况下,该方法的做法就是如此。不过,在另一些时候,其所属值的已读计数会等于已写计数,这表明:此时的缓冲区中已经没有任何未读的字节了。
  • 当缓冲区中已无未读字节时,Read方法会先检查参数p的长度是否大于或等于缓冲区的长度。如果是,那么Read方法会索性放弃向缓冲区中填充数据,转而直接从其底层读取器中读出数据并拷贝到p中。这意味着它完全跨过了缓冲区,并直连了数据供需的双方。

需要注意的是,Peek方法在遇到类似情况时的做法与这里的区别(这两种做法孰优孰劣还要看具体的使用场景)。

Peek方法会在条件满足时填充缓冲区,并在发现参数n的值比缓冲区的长度更大时,直接返回缓冲区中的所有未读字节。

如果我们当初设定的缓冲区长度很大,那么在这种情况下的方法执行耗时,就有可能会比较长。最主要的原因是填充缓冲区需要花费较长的时间。

由fill方法执行的流程可知,它会尽量填满缓冲区中的可写空间。然而,Read方法在大多数的情况下,是不会向缓冲区中写入数据的,尤其是在前面描述的那种情况下,即:缓冲区中已无未读字节,且参数p的长度大于或等于缓冲区的长度。

此时,该方法会直接从底层读取器那里读出数据,所以数据的读出速度就成为了这种情况下方法执行耗时的决定性因素。

当然了,我在这里说的只是耗时操作在某些情况下更可能出现在哪里,一切的结论还是要以性能测试的客观结果为准。

说回Read方法的内部流程。如果缓冲区中已无未读字节,但其长度比参数p的长度更大,那么该方法会先把已读计数和已写计数的值都重置为0,然后再尝试着使用从底层读取器那里获取的数据,对缓冲区进行一次从头至尾的填充。

不过要注意,这里的尝试只会进行一次。无论在这一时刻是否能够获取到数据,也无论获取时是否有错误发生,都会是如此。而fill方法的做法与此不同,只要没有发生错误,它就会进行多次尝试,因此它真正获取到一些数据的可能性更大。

不过,这两个方法有一点是相同,那就是:只要它们把获取到的数据写入缓冲区,就会及时地更新已写计数的值。

再来说ReadSlice方法和ReadBytes方法。 这两个方法的功能总体上来说,都是持续地读取数据,直至遇到调用方给定的分隔符为止。

ReadSlice方法会先在其缓冲区的未读部分中寻找分隔符。如果未能找到,并且缓冲区未满,那么该方法会先通过调用fill方法对缓冲区进行填充,然后再次寻找,如此往复。

如果在填充的过程中发生了错误,那么它会把缓冲区中的未读部分作为结果返回,同时返回相应的错误值。

注意,在这个过程中有可能会出现虽然缓冲区已被填满,但仍然没能找到分隔符的情况。

这时,ReadSlice方法会把整个缓冲区(也就是buf字段代表的字节切片)作为第一个结果值,并把缓冲区已满的错误(即bufio.ErrBufferFull变量的值)作为第二个结果值。

经过fill方法填满的缓冲区肯定从头至尾都只包含了未读的字节,所以这样做是合理的。

当然了,一旦ReadSlice方法找到了分隔符,它就会在缓冲区上切出相应的、包含分隔符的字节切片,并把该切片作为结果值返回。无论分隔符找到与否,该方法都会正确地设置已读计数的值。

比如,在返回缓冲区中的所有未读字节,或者代表全部缓冲区的字节切片之前,它会把已写计数的值赋给已读计数,以表明缓冲区中已无未读字节。

如果说ReadSlice是一个容易半途而废的方法的话,那么可以说ReadBytes方法算得上是相当的执着。

ReadBytes方法会通过调用ReadSlice方法一次又一次地从缓冲区中读取数据,直至找到分隔符为止。

在这个过程中,ReadSlice方法可能会因缓冲区已满而返回所有已读到的字节和相应的错误值,但ReadBytes方法总是会忽略掉这样的错误,并再次调用ReadSlice方法,这使得后者会继续填充缓冲区并在其中寻找分隔符。

除非ReadSlice方法返回的错误值并不代表缓冲区已满的错误,或者它找到了分隔符,否则这一过程永远不会结束。

如果寻找的过程结束了,不管是不是因为找到了分隔符,ReadBytes方法都会把在这个过程中读到的所有字节,按照读取的先后顺序组装成一个字节切片,并把它作为第一个结果值。如果过程结束是因为出现错误,那么它还会把拿到的错误值作为第二个结果值。

在bufio.Reader类型的众多读取方法中,依赖ReadSlice方法的除了ReadBytes方法,还有ReadLine方法。不过后者在读取流程上并没有什么特别之处,我就不在这里赘述了。

另外,该类型的ReadString方法完全依赖于ReadBytes方法,前者只是在后者返回的结果值之上做了一个简单的类型转换而已。

最后,我还要提醒你一下,有个安全性方面的问题需要你注意。bufio.Reader类型的Peek方法、ReadSlice方法和ReadLine方法都有可能会造成内容泄露。

这主要是因为它们在正常的情况下都会返回直接基于缓冲区的字节切片。我在讲bytes.Buffer类型的时候解释过什么叫内容泄露。你可以返回查看。

调用方可以通过这些方法返回的结果值访问到缓冲区的其他部分,甚至修改缓冲区中的内容。这通常都是很危险的。

package main

import (
"bufio"
"fmt"
"strings"
) func main() {
comment := "Package bufio implements buffered I/O. " +
"It wraps an io.Reader or io.Writer object, " +
"creating another object (Reader or Writer) that " +
"also implements the interface but provides buffering and " +
"some help for textual I/O."
basicReader := strings.NewReader(comment)
fmt.Printf("The size of basic reader: %d\n", basicReader.Size()) size := 300
fmt.Printf("New a buffered reader with size %d ...\n", size)
reader1 := bufio.NewReaderSize(basicReader, size)
fmt.Println() fmt.Print("[ About 'Peek' method ]\n\n")
// 示例1。
peekNum := 38
fmt.Printf("Peek %d bytes ...\n", peekNum)
bytes, err := reader1.Peek(peekNum)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
fmt.Println() fmt.Print("[ About 'Read' method ]\n\n")
// 示例2。
readNum := 38
buf1 := make([]byte, readNum)
fmt.Printf("Read %d bytes ...\n", readNum)
n, err := reader1.Read(buf1)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", n, buf1)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
fmt.Println() fmt.Print("[ About 'ReadSlice' method ]\n\n")
// 示例3。
fmt.Println("Reset the basic reader ...")
basicReader.Reset(comment)
fmt.Println("Reset the buffered reader ...")
reader1.Reset(basicReader)
fmt.Println() delimiter := byte('(')
fmt.Printf("Read slice with delimiter %q...\n", delimiter)
line, err := reader1.ReadSlice(delimiter)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", len(line), line)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
fmt.Println() delimiter = byte('[')
fmt.Printf("Read slice with delimiter %q...\n", delimiter)
line, err = reader1.ReadSlice(delimiter)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", len(line), line)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader1.Buffered())
fmt.Println() // 示例4。
fmt.Println("Reset the basic reader ...")
basicReader.Reset(comment)
size = 200
fmt.Printf("New a buffered reader with size %d ...\n", size)
reader2 := bufio.NewReaderSize(basicReader, size)
fmt.Println() delimiter = byte('[')
fmt.Printf("Read slice with delimiter %q...\n", delimiter)
line, err = reader2.ReadSlice(delimiter)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", len(line), line)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader2.Buffered())
fmt.Println() fmt.Print("[ About 'ReadBytes' method ]\n\n")
// 示例5。
fmt.Println("Reset the basic reader ...")
basicReader.Reset(comment)
size = 200
fmt.Printf("New a buffered reader with size %d ...\n", size)
reader3 := bufio.NewReaderSize(basicReader, size)
fmt.Println() delimiter = byte('[')
fmt.Printf("Read bytes with delimiter %q...\n", delimiter)
line, err = reader3.ReadBytes(delimiter)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", len(line), line)
fmt.Printf("The number of unread bytes in the buffer: %d\n", reader3.Buffered())
fmt.Println() // 示例6和示例7。
fmt.Print("[ About contents leak ]\n\n")
showContentsLeak(comment)
} func showContentsLeak(comment string) {
// 示例6。
basicReader := strings.NewReader(comment)
fmt.Printf("The size of basic reader: %d\n", basicReader.Size()) size := len(comment)
fmt.Printf("New a buffered reader with size %d ...\n", size)
reader4 := bufio.NewReaderSize(basicReader, size)
fmt.Println() peekNum := 7
fmt.Printf("Peek %d bytes ...\n", peekNum)
bytes, err := reader4.Peek(peekNum)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
fmt.Println() // 只要扩充一下之前拿到的字节切片bytes,
// 就可以用它来读取甚至修改缓冲区中的后续内容。
bytes = bytes[:cap(bytes)]
fmt.Printf("The all of the contents in the buffer:\n%q\n", bytes)
fmt.Println() blank := byte(' ')
fmt.Println("Set blanks into the contents in the buffer ...")
for _, i := range []int{55, 56, 57, 58, 66, 67, 68} {
bytes[i] = blank
}
fmt.Println() peekNum = size
fmt.Printf("Peek %d bytes ...\n", peekNum)
bytes, err = reader4.Peek(peekNum)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Peeked contents(%d):\n%q\n", len(bytes), bytes)
fmt.Println() // 示例7。
// ReadSlice方法也存在相同的问题。
delimiter := byte(',')
fmt.Printf("Read slice with delimiter %q...\n", delimiter)
line, err := reader4.ReadSlice(delimiter)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Read contents(%d): %q\n", len(line), line)
fmt.Println() line = line[:cap(line)]
fmt.Printf("The all of the contents in the buffer:\n%q\n", line)
fmt.Println() underline := byte('_')
fmt.Println("Set underlines into the contents in the buffer ...")
for _, i := range []int{89, 92, 103} {
line[i] = underline
}
fmt.Println() peekNum = size
fmt.Printf("Peek %d bytes ...\n", peekNum)
bytes, err = reader4.Peek(peekNum)
if err != nil {
fmt.Printf("error: %v\n", err)
}
fmt.Printf("Peeked contents(%d): %q\n", len(bytes), bytes)
}

总结

我们用比较长的篇幅介绍了bufio包中的数据类型,其中的重点是bufio.Reader类型。

bufio.Reader类型代表的是携带缓冲区的读取器。它的值在被初始化的时候需要接受一个底层的读取器,后者的类型必须是io.Reader接口的实现。

Reader值中的缓冲区其实就是一个数据存储中介,它介于底层读取器与读取方法及其调用方之间。此类值的读取方法一般都会先从该值的缓冲区中读取数据,同时在必要的时候预先从其底层读取器那里读出一部分数据,并填充到缓冲区中以备后用。填充缓冲区的操作通常会由该值的fill方法执行。

在填充的过程中,fill方法有时还会对缓冲区进行压缩。在Reader值拥有的众多读取方法中,有 4 个方法可以作为不同读取流程的代表,它们是:Peek、Read、ReadSlice和ReadBytes。

Peek方法的特点是即使读取了缓冲区中的数据,也不会更改已读计数的值。而Read方法会在参数值的长度过大,且缓冲区中已无未读字节时,跨过缓冲区并直接向底层读取器索要数据。

ReadSlice方法会在缓冲区的未读部分中寻找给定的分隔符,并在必要时对缓冲区进行填充。

如果在填满缓冲区之后仍然未能找到分隔符,那么该方法就会把整个缓冲区作为第一个结果值返回,同时返回缓冲区已满的错误。

ReadBytes方法会通过调用ReadSlice方法,一次又一次地填充缓冲区,并在其中寻找分隔符。除非发生了未预料到的错误或者找到了分隔符,否则这一过程将会一直进行下去。

Reader值的ReadLine方法会依赖于它的ReadSlice方法,而其ReadString方法则完全依赖于ReadBytes方法。

另外,值得我们特别注意的是,Reader值的Peek方法、ReadSlice方法和ReadLine方法都可能会造成其缓冲区中的内容的泄露。

最后再说一下bufio.Writer类型。把该类值的缓冲区中暂存的数据写进其底层写入器的功能,主要是由它的Flush方法实现的。

此类值的所有数据写入方法都会在必要的时候调用它的Flush方法。一般情况下,这些写入方法都会先把数据写进其所属值的缓冲区,然后再增加该值中的已写计数。但是,在有些时候,Write方法和ReadFrom方法也会跨过缓冲区,并直接把数据写进其底层写入器。

请记住,虽然这些写入方法都会不时地调用Flush方法,但是在写入所有的数据之后再显式地调用一下这个方法总是最稳妥的。

package main

import (
"bufio"
"bytes"
"fmt"
"strings"
) func main() {
comment := "Writer implements buffering for an io.Writer object. " +
"If an error occurs writing to a Writer, " +
"no more data will be accepted and all subsequent writes, " +
"and Flush, will return the error. After all data has been written, " +
"the client should call the Flush method to guarantee all data " +
"has been forwarded to the underlying io.Writer."
basicWriter1 := &strings.Builder{} size := 300
fmt.Printf("New a buffered writer with size %d ...\n", size)
writer1 := bufio.NewWriterSize(basicWriter1, size)
fmt.Println() // 示例1。
begin, end := 0, 53
fmt.Printf("Write %d bytes into the writer ...\n", end-begin)
writer1.WriteString(comment[begin:end])
fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
fmt.Printf("The number of unused bytes in the buffer: %d\n",
writer1.Available())
fmt.Println("Flush the buffer in the writer ...")
writer1.Flush()
fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
fmt.Printf("The number of unused bytes in the buffer: %d\n",
writer1.Available())
fmt.Println() // 示例2。
begin, end = 0, 326
fmt.Printf("Write %d bytes into the writer ...\n", end-begin)
writer1.WriteString(comment[begin:end])
fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
fmt.Printf("The number of unused bytes in the buffer: %d\n",
writer1.Available())
fmt.Println("Flush the buffer in the writer ...")
writer1.Flush()
fmt.Println() // 示例3。
basicWriter2 := &bytes.Buffer{}
fmt.Printf("Reset the writer with a bytes buffer(an implementation of io.ReaderFrom) ...\n")
writer1.Reset(basicWriter2)
reader := strings.NewReader(comment)
fmt.Println("Read data from the reader ...")
writer1.ReadFrom(reader)
fmt.Printf("The number of buffered bytes: %d\n", writer1.Buffered())
fmt.Printf("The number of unused bytes in the buffer: %d\n",
writer1.Available())
}

思考题

今天的思考题是:bufio.Scanner类型的主要功用是什么?它有哪些特点?

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用二十一)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  3. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  4. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  5. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  6. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  7. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  8. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  9. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

  10. Go语言核心36讲(Go语言实战与应用二十四)--学习笔记

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. C++的指针使用心得

    使用C++有一段时间了,C++的手动内存管理缺失很麻烦,一不小心容易产生内存泄漏.自己总结了一点使用原则(不一定对),备注一下,避免忘记. 1.类外部传来的指针不处理 2.Qt对象管理的内存不处理 3 ...

  2. Vue | uni-app 中使用websocket

    @ 目录 首先在根目录下新建一个store文件夹,并新建一个websocket.js文件,代码如下: import Vue from 'vue' import Vuex from 'vuex' Vue ...

  3. 从零开始的DIY智能家居 - 基于 ESP32 的智能水浊度传感器

    前言 家里有个鱼缸养了几条鱼来玩玩,但是换水的问题着实头疼,经常一个不注意就忘记换水,鱼儿就没了.o(╥﹏╥)o 在获得 Spirit 1 边缘计算机 后就相当于有了一个人智能设备服务器,可以自己开发 ...

  4. docker 简单总结

    一.docker 安装 yum 方式在centos和rhce上的安装条件: 要安装Docker引擎,你需要一个维护版本的CentOS 7或8.不支持或测试存档版本.必须启用centos-extras存 ...

  5. 启用或禁用普通用户Exchange Online PowerShell功能

    默认情况下,允许在 Microsoft 365 中创建的所有帐户都可以使用 Exchange Online PowerShell.管理员可以使用 Exchange Online PowerShell ...

  6. c++ 算法 next_permutation

    遇到这个算法是在大牛写的10行的8皇后问题中,下面首先给出这个10行就解决了8皇后的NB代码,我目前还是没有看懂对于皇后不在同一列的判断,因为他巧妙的用了移位操作. #include<iostr ...

  7. Spark的安装及其配置

    1.Spark下载 https://archive.apache.org/dist/spark/ 2.上传解压,配置环境变量 配置bin目录 解压:tar -zxvf spark-2.4.5-bin- ...

  8. yum设置取消代理

    unset http_proxy unset https_proxy 暂时取消代理,若永久取消代理,需要修改/etc/yum.conf 去掉 proxy=http://proxyhost:8080

  9. [第二章]c++学习笔记2(类和对象的基础3)

    隐藏的概念 隐藏的作用 使用例 成员函数的重载与缺省(附使用例) 注意事项

  10. PTA7-2 愿天下有情人都是失散多年的兄妹

    呵呵.大家都知道五服以内不得通婚,即两个人最近的共同祖先如果在五代以内(即本人.父母.祖父母.曾祖父母.高祖父母)则不可通婚.本题就请你帮助一对有情人判断一下,他们究竟是否可以成婚? 输入格式: 输入 ...