bufio读取原理
将文件内容预读取到缓存--bufio
这里的思想很简单,当用户从文件中读取数据的时候,先从文件中读取一大块内容到内存缓冲区,以供后面的读取操作直接从内存缓冲区进行读取,以降低从文件中读取的系统调用次数。如下图所示:
缓冲区的状态
缓冲区有三种状态,分别是缓冲区为空、缓冲区未满但有可读取的数据以及缓冲区满的状态。在bufio中,缓冲区本质上是一个字节切片,并通过两个整型变量r和w分别表示可读取以及可写入的索引位置。从文件中每加载一个字节的内容到缓冲区则w+1,从缓冲区每读走一个字节的内容,则r+1。下面我们分别看下三种状态。
1)缓冲区为空的状态
缓冲区为空的状态本质上是指没有内容可读。其判断标准如下:r == w
r和w相等,意味着已经将写入到缓冲区的内容都读完了。其中最简单的就是r和w都等于0,缓冲区中没有任何内容。另一种是缓冲区中有内容,但已经都被读取走了,即r和w相等但不等于0.在这种状态下,当再需要读取内容时,会首先将r和w都置为0,然后从文件中加载新的数据填充到缓冲区中以供下次调用方读取。
2)缓冲区为非空的状态
这种状态是指在缓冲区中有可读的内容,其判断标准如下:r != w && (w-r) < len(buf)
3)缓冲区满状态
还有最后一种缓冲区状态,即缓冲区满。其通用的判断标准如下:(w-r) >= len(buf)
如果要满足上述公式,只有一种情况,即 r=0,表示还没有从缓冲区读走任何内容。w=len(buf),表示从文件中读取的内容已经填满了整个缓冲区。该示例中为w=10,即表示没有任何空闲的空间。
读取特定字节数的操作-- Read([]byte)
package main import ( "bufio" "fmt" "os" ) func main() { filename := "./test.txt" //以读写模式打开文件 fd, _ := os.OpenFile(filename, os.O_RDWR, 0666) //将fd包装到buffer Reader中 bufioReader := bufio.NewReader(fd) p := make([]byte, 2) n, _ := bufioReader.Read(p) fmt.Printf("n=%d,p=%s", n, p) }
基本用法看起来和直接从文件中读取差不多,只不过是多包装了一层buffer Reader。下面我们看看其内部的具体实现。上面提到bufio的基本思想是有一个缓冲区,调用方直接从缓冲区中读取。下图是其初始的状态:
场景一:当缓冲区为空状态时的读取逻辑(即r等于w)
在缓冲区为空时,进行读取也有两种情况:
- 若调用方要读取的字节数 小于 缓冲区的长度,则先填充缓冲区,再从缓冲区中读取。
- 若调用方要读取的字节数 ≥ 缓冲区的长度,则直接从文件中读取,不填充缓冲区。
场景二:当缓冲区为非空状态的读取逻辑
如果在缓冲区非空状态下进行读取操作时,唯一需要注意的点就是当缓冲区中可读取的内容字节数小于调用者要读取的字节数时,则只能读取缓冲区中的内容。最后,将实际读取到的字节数返回给调用者,并将下次可读取的索引位置 r 进行更新。这时缓冲区的状态实际上是变成了空的状态。如果再继续读取的话,r和w就会复位成0,并从文件中再读取一大块内容填充到缓冲区中。另外还有一种就是缓冲区满的状态下的读取逻辑,这种场景下就结合场景二进行读取即可。以上实际上就是bufio包中的Read([]byte)函数的逻辑,按字节读取。
5 从缓冲区中读取到指定位置
这种读取方式是从缓冲区中读取数据,直到遇到指定的字符为止(实际上是指定字符所在的切片索引位置)。按行读取是最常见的场景之一,即一直读取到换行符为止。这种读取方式中也分两种情况:
- 情况1:当缓冲区中包含指定的字符,则从缓冲区中直接返回包含该字符及之前的有效内容。
- 情况2:当缓冲区中没有指定的字符,又分两种情况:
- 2.1 若缓冲区是满的状态,则返回整个缓冲的内容
- 2.2 若缓冲区处于非空状态(也非满的状态),则将缓冲区填满内容,再从该缓冲区中查找是否存在指定的字符。若在缓冲区中能够查找到指定的字符,则返回该指定字符及以前的内容,否则,返回整个缓冲区的内容,即2.1的情况。
情况1比较简单,假设我们在缓冲区中读取内容,直到遇到指定字符 E
为止。缓冲区中的状态如下:
缓冲区中的buf[1:5]这段内容中包含字符E,那么直接返回buf[1:2]的内容即可。
情况2稍微复杂下。下面我们稍微拆解下在缓冲区各种状态下第一次未找到对应的字符的情况。首先我们看当缓冲区处于满的状态下,第一次未找到对应字符的逻辑。如下:
若在缓冲区中查找字符B
,发现没有对应的字符,同时又发现缓冲区状态是满的状态,所以就直接将缓冲区中的所有内容都返回,同时将缓冲区满的错误返回给调用者。如下图,则返回给调用者 HELLOGO!
,同时返回errors.New("buffer is full"):
其次,我们看下如果缓冲区里有内容,但未满的状态的查找逻辑。假设缓冲区中下次可读的位置在第5个,如下图:
我们从缓冲区的索引5的位置开始查找字符B
,发现没有找到。但同时也发现缓冲区处于非满的状态,因为从索引0到索引5之间的内容已经被读取走了,所以这段内存相当于处于空闲的状态。因此,这里会先将缓冲区中5到8之间的内容移动到0到3之间,然后再从文件中读取一段内容填充到缓冲3到8的位置上,最后继续查找。如下图:
上图是移动完内容之后的结果。然后从文件中读取内容填充剩余的缓冲区,如下图:
这样,缓冲区中又有了新的内容,则会从新的内容中继续查找指定的字符B,如下图:
这里要注意的是0-3之间的内容不再重复查找,只会从3-8之间查找。这里找到了字符B的位置在buf[3:8]这段内容的位置0处。最后返回buf[0:4]的这段内容,因为之前的内容也是搜索的一部分。如下图:
以上在缓冲区中移动内容到开始位置,并重新填充内容到缓冲区的过程实际上就是bufio包中的fill方法。而整个按指定字符读取的过程是bufio包中的ReadLine和ReadSlice函数的对应实现(ReadLine函数调用了ReadSlice函数)。ReadLine函数默认是读取内容,直到遇到第一个换行符\n为止。我们注意到以上的ReadLine和ReadSlice函数都是在缓冲区中的内容中搜索。只要在缓冲区满的状态下,无论是能否搜索到对应的字符,都会返回。我们知道,文件内容的大小一般都会远远大于缓冲区的大小,那如果在缓冲区满的状态下没有找到对应的字符,如何继续往下查找呢?
从全文件中读取到指定位置
这种读取方式是从缓冲区中读取,如果该缓冲区中没有读到指定的字符,那么就将该缓冲区的内容暂存到一个临时区,然后再读取文件将缓冲区填满,再次查找,依次循环,直到读到指定的字符为止或读到文件的末尾,将所有的结果返回给调用者。假设缓冲区处于满的状态,我们要查找指定的字符 r
第一步先从缓冲区中查找,如下:
第二步,在缓冲区中未找到指定的字符 r,所以需要将缓冲区中的内容移动到暂存区存储起来,以备后续返回时用,如下图:
第三步,这时缓冲区实际处于空的状态,然后需要从文件中读取内容再次填充缓冲区,继续查找 是否有 r字符。如下图:
第四步,继续从缓冲区中查找字符字符r,如果找到了,则将暂存区中的内容及缓冲区中r及之前的内容都返回给调用者。如下图:
如果在第四步中依然没找到指定的字符r,那么就会调回第二步,依次循环,直到找到指定的字符或将文件中所有的内容都扫描完为止,最终将暂存区即缓冲区中的内容都返回。此过程就是bufio中的ReadString函数即对应的collectFragments函数的实现。
bufio写缓存原理
type Writer struct { err error buf []byte n int wr io.Writer }
bufio.Write(p []byte)
的思路如下
- 判断buf中可用容量是否可以放下 p
- 如果能放下,直接把p拼接到buf后面,即把内容放到缓冲区
- 如果缓冲区的可用容量不足以放下,且此时缓冲区是空的,直接把p写入文件即可
- 如果缓冲区的可用容量不足以放下,且此时缓冲区有内容,则用p把缓冲区填满,把缓冲区所有内容写入文件,并清空缓冲区
- 判断p的剩余内容大小能否放到缓冲区,如果能放下(此时和步骤1情况一样)则把内容放到缓冲区
- 如果p的剩余内容依旧大于缓冲区,(注意此时缓冲区是空的,情况和步骤3一样)则把p的剩余内容直接写入文件
标签:字符,读取,状态,缓冲区,内容,bufio,go From: https://www.cnblogs.com/dadishi/p/17060916.html