github.com/code-reading/golang@v0.0.0-20220303082512-ba5bc0e589a3/docs/001-bufio包源码分析.md (about) 1 2  3 4 [分析示例:coding/bufio](../coding/bufio) 5 6 [源码位置:/src/bufio](../go/src/bufio) 7 8 ## bufio包介绍 9 bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。 10 11 ## bufio 是通过缓冲来提高效率 12 13 os包提供的Write方法是没有缓存功能的, 它会直接写到磁盘上, 相比bufio 先缓存一定的数据在统一写入到磁盘中, 不带缓冲功能的Write方法会带来额外的IO开销; 14 15 简单的说就是,把文件读取进缓冲(内存)之后再读取的时候就可以避免文件系统的io 从而提高速度。同理,在进行写操作时,先把文件写入缓冲(内存),然后由缓冲写入文件系统。看完以上解释有人可能会表示困惑了,直接把 内容->文件 和 内容->缓冲->文件相比, 缓冲区好像没有起到作用嘛。其实缓冲区的设计是为了存储多次的写入,最后一口气把缓冲区内容写入文件。 16 17 bufio 封装了io.Reader或io.Writer接口对象,并创建另一个也实现了该接口的对象 18 19 io.Reader或io.Writer 接口实现read() 和 write() 方法,对于实现这个接口的对象都是可以使用这两个方法的 20 21  22 23 ## bufio 源码分析 24 25 ### bufio.Read 26 27 bufio.Read(p []byte) 相当于读取大小len(p)的内容,思路如下: 28 29 - 当缓存区有内容的时,将缓存区内容全部填入p并清空缓存区 30 - 当缓存区没有内容的时候且len(p)>len(buf),即要读取的内容比缓存区还要大,直接去文件读取即可 31 - 当缓存区没有内容的时候且len(p)<len(buf),即要读取的内容比缓存区小,缓存区从文件读取内容充满缓存区,并将p填满(此时缓存区有剩余内容) 32 - 以后再次读取时缓存区有内容,将缓存区内容全部填入p并清空缓存区(此时和情况1一样) 33 34 > reader内部通过维护一个r, w 即读入和写入的位置索引来判断是否缓存区内容被全部读出 35 36 ### bufio.Write 37 38 bufio.Write(p []byte) 的思路如下 39 40 - 判断buf中可用容量是否可以放下 p 41 - 如果能放下,直接把p拼接到buf后面,即把内容放到缓冲区 42 - 如果缓冲区的可用容量不足以放下,且此时缓冲区是空的,直接把p写入文件即可 43 - 如果缓冲区的可用容量不足以放下,且此时缓冲区有内容,则用p把缓冲区填满,把缓冲区所有内容写入文件,并清空缓冲区 44 - 判断p的剩余内容大小能否放到缓冲区,如果能放下(此时和步骤1情况一样)则把内容放到缓冲区 45 - 如果p的剩余内容依旧大于缓冲区,(注意此时缓冲区是空的,情况和步骤2一样)则把p的剩余内容直接写入文件 46 47 ### 深入理解 bufio.SplitFunc 48 49 ```go 50 type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error) 51 ``` 52 53 Scanner是有缓存的,意思是Scanner底层维护了一个Slice用来保存已经从Reader中读取的数据,Scanner会调用我们设置SplitFunc,将缓冲区内容(data)和是否已经输入完了(atEOF)以参数的形式传递给SplitFunc,而SplitFunc的职责就是根据上述的两个参数返回下一次Scan需要前进几个字节(advance),分割出来的数据(token),以及错误(err)。 54 55 这是一个通信双向的过程,Scanner告诉我们的SplitFunc已经扫描到的数据和是否到结尾了,我们的SplitFunc则根据这些信息将分割的结果返回和下次扫描需要前进的位置返回给Scanner。用一个例子来说明: 56 57 ```go 58 package main 59 import ( 60 "bufio" 61 "fmt" 62 "strings" 63 ) 64 func main() { 65 input := "abcdefghijkl" 66 scanner := bufio.NewScanner(strings.NewReader(input)) 67 split := func(data []byte, atEOF bool) (advance int, token []byte, err error) { 68 fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data) 69 return 0, nil, nil 70 } 71 scanner.Split(split) 72 buf := make([]byte, 2) 73 scanner.Buffer(buf, bufio.MaxScanTokenSize) 74 for scanner.Scan() { 75 fmt.Printf("%s\n", scanner.Text()) 76 } 77 } 78 ``` 79 80 output 81 82 ``` 83 false 2 ab 84 false 4 abcd 85 false 8 abcdefgh 86 false 12 abcdefghijkl 87 true 12 abcdefghijkl 88 ``` 89 90 这里我们把缓冲区的初始大小设置为了2,不够的时候会扩展为原来的2倍,最大为bufio.MaxScanTokenSize,这样一开始扫描2个字节,我们的缓冲区就满了,reader的内容还没有读取到EOF,然后split函数执行,输出: 91 92 ``` 93 false 2 ab 94 ``` 95 紧接着函数返回 0, nil, nil这个返回值告诉Scanner数据不够,下次读取的位置前进0位,需要继续从reader里面读取,此时因为缓冲区满了,所以容量扩展为2 * 2 = 4,reader的内容还没有读取到EOF,输出 96 97 ``` 98 false 4 abcd 99 ``` 100 101 重复上述步骤,一直到最后全部内容读取完了,EOF此时变成了true 102 103 ``` 104 true 12 abcdefghijkl 105 ``` 106 107 ### 深入理解bufio.Write方法 108 bufio包实现了带缓冲的I/O,它封装了io.Reader和io.Writer对象,然后创建了另外一种对象(Reader或Writer)实现了相同的接口,但是增加了缓冲功能。 109 110 首先来看没有缓冲功能的Write(os包中)方法,它会将数据直接写到文件中。 111 112 ```go 113 package main 114 115 import ( 116 "os" 117 "fmt" 118 ) 119 120 func main() { 121 file, err := os.OpenFile("a.txt", os.O_CREATE|os.O_RDWR, 0666) 122 if err != nil { 123 fmt.Println(err) 124 } 125 defer file.Close() 126 127 content := []byte("hello world!") 128 if _, err = file.Write(content); err != nil { 129 fmt.Println(err) 130 } 131 fmt.Println("write file successful") 132 } 133 ``` 134 135 接着看一个错误的使用带缓冲的Write方法例子,当下面的程序执行后是看不到写入的数据的。 136 137 ```go 138 package main 139 140 import ( 141 "os" 142 "fmt" 143 "bufio" 144 ) 145 146 func main() { 147 file, err := os.OpenFile("a.txt", os.O_CREATE|os.O_RDWR, 0666) 148 if err != nil { 149 fmt.Println(err) 150 } 151 defer file.Close() 152 153 content := []byte("hello world!") 154 newWriter := bufio.NewWriter(file) 155 if _, err = newWriter.Write(content); err != nil { 156 fmt.Println(err) 157 } 158 fmt.Println("write file successful") 159 } 160 ``` 161 为什么会在文件中看不到写入的数据呢, Write方法首先会判断写入的数据长度是否大于设置的缓冲长度,如果小于,则会将数据copy到缓冲中;当数据长度大于缓冲长度时,如果数据特别大,则会跳过copy环节,直接写入文件。其他情况依然先会将数据拷贝到缓冲队列中,然后再将缓冲中的数据写入到文件中。 162 163 所以上面的错误示例,只要给其添加Flush()方法,将缓存的数据写入到文件中。 164 165 ```go 166 package main 167 168 import ( 169 "os" 170 "fmt" 171 "bufio" 172 ) 173 174 func main() { 175 file, err := os.OpenFile("./a.txt", os.O_CREATE|os.O_RDWR, 0666) 176 if err != nil { 177 fmt.Println(err) 178 } 179 defer file.Close() 180 181 content := []byte("hello world!") 182 newWriter := bufio.NewWriterSize(file, 1024) 183 if _, err = newWriter.Write(content); err != nil { 184 fmt.Println(err) 185 } 186 if err = newWriter.Flush(); err != nil { 187 fmt.Println(err) 188 } 189 fmt.Println("write file successful") 190 } 191 ``` 192 193 ## 注意事项 194 195 默认扫描方式按行扫描 ScanLines. 196 197 单次扫描文本最大值是64k MaxScanTokenSize = 64 * 1024 198 199 除非能确定行长度不超过65536,否则不要使用bufio.Scanner!如果建议使用ReadBytes或ReadString 200 201 当然也可以在调用Scan()之前设置buffer和MaxScanTokenSize的大小 比如: scanner.Buffer([]byte{}, bufio.MaxScanTokenSize*10) 202 203 按行去读取数据在 bufio.Reader 中也提供了 ReadLine() 或 ReadString('\n') 或 ReadBytes('\n') 可供选择 204 205 ## 陷阱分析 206 207 ### 丢数据 208 209 > 特殊情况需要注意的就是: 当数据末尾没有\n的时候,直到EOF还没有分隔符\n,这时候返回EOF错误,但是line里面还是有数据的,如果不处理的话就会漏掉最后一行. 210 211 ```go 212 s := "a\nb\nc" 213 reader := bufio.NewReader(strings.NewReader(s)) 214 for { 215 line, err := reader.ReadString('\n') 216 if err != nil { 217 if err == io.EOF { 218 // **处理当数据末尾没有\n的情况** 219 fmt.Printf("%#v\n", line) 220 break 221 } 222 panic(err) 223 } 224 fmt.Printf("%#v\n", line) 225 } 226 // 关键点:最后一次返回的错误是 io.EOF,line 里面是可能有数据的。这样不会遗漏最后一行, 227 buf := bufio.NewReader(bytes.NewReader(r)) 228 for { 229 line, err := buf.ReadString('\n') 230 if err == nil || err == io.EOF { 231 line = strings.TrimSpace(line) 232 if line != "" { 233 fmt.Println(line) 234 } 235 } 236 if err != nil { 237 break 238 } 239 } 240 ``` 241 242 ### Scanner 初始化buffer size 243 244 > 通常情况下建议使用Scanner, 如果能预估buffer大小,最后设置合适的buffer 245 246 ```go 247 // Split functions defaults to ScanLines 248 249 // ScanBytes is a split function for a Scanner that returns each byte as a token. 250 // data []byte 待处理扫描的数据 251 // atEOF bool 判断数据是否处理完成 252 253 // advance int 返回要处理的字节数 254 // token []byte 保存要处理的内容 255 // 256 func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error) { 257 if atEOF && len(data) == 0 { 258 return 0, nil, nil 259 } 260 return 1, data[0:1], nil 261 } 262 263 func main(){ 264 scanner:=bufio.NewScanner(strings.NewReader("ABCDEFG\nHIJKELM"),) 265 // 可以在调用Scan之前设置buffer和MaxScanTokenSize的大小 266 scanner.Buffer([]byte{}, bufio.MaxScanTokenSize*10) 267 for scanner.Scan(){ 268 fmt.Println(scanner.Text()) // scanner.Bytes() 269 } 270 // 通过 scanner.Err(); 我们可以捕捉到 扫描中的错误信息,这对单行文件超过 MaxScanTokenSize 时特别有用 271 if err := scanner.Err(); err != nil { 272 fmt.Fprintln(os.Stderr, "reading standard input:", err) 273 } 274 } 275 ``` 276 277 bufio的Reader和Writer操作一般适用于磁盘IO的读写场景。通过bufio标准库,实现对输入数据的读取、处理和输出操作。 278 279 ### Reader buffersize 初始化 280 281 ```go 282 reader:=bufio.NewReader(oldReader) // 默认size为4096 283 reader:=bufio.NewReaderSize(oldReader, size) 284 285 bytes, error = reader.ReadSlice(delim) /*结束符*/ 286 bytes, error = reader.ReadString(delim) 287 bytes, error = reader.ReadLine() // 默认换行符\n 288 bytes, error = reader.ReadBytes(delim) 289 n, error = reader.Reader([]byte) 290 291 var bts = make([]byte, 100) // 初始化,保证有足够写入空间 292 reader:=bufio.NewReader(strings.NewReader("ABCDEFG\nHIJKLMN\n") 293 _, err = reader.Read(bts) //传递写入副本 294 ``` 295 这里要平时注意的一点:var bts = make([]byte, 100), 需要初始化。 296 slice类型变量首先要初始化, 如果不初始化,则如果在调用的方法体内初始化,则返回时,slice类型变量还是旧值。 297 298 > slice底层实际上是struct结构体类型,有len,cap和指针数组组成。所以函数体内初始化则内存地址和传入的内存地址不是一致的,读到的数据是旧的 299 > 如果传入参数是初始化的,则相当于func(temp=Struct{}/传入参数param/), 虽然地址不一样,但是重新赋值了,且len、cap值相同,底层的指针数组还是指向一样的内存地址,如果底层数组的内存地址存放的数据发生变化,由于指向数组的指针没有变化,则传入参数可是指向了底层数组新的数据。 300 301 302 ### func (b *Writer) Write(p []byte) (int, error)方法的正确理解 303 304 ``` 305 如果内存缓冲区剩余空间小于len(p),分两种情况讨论: 306 如果当前内存缓冲区为空,则直接把p数据写入到磁盘IO,b.wr.Write(p); 307 如果当前内存缓冲区不空,则首先把缓冲区填满,然后先把内存缓冲区的数据进行一次磁盘IO回写操作,之后内存缓冲区可用大小有是len(Writer.buf)长度了,这时又分两种情况讨论:第一种:如果剩余要处理的p数据小于内存缓冲区的大小, 则把剩余数据p写入到内存缓冲区;第二种:如果剩余要处理的p数据大于等于内存缓冲区,则没必要缓冲了,直接整体一次回写到磁盘。 308 如果内存缓冲区剩余空间大于等于len(p), 则先把数据暂存到缓冲区,减少磁盘IO。 309 ``` 310