golang bufio包就这么用

bufio

它的作用用一句话表述就是:

利用缓冲区减少io操作次数,提升读写性能。

1. 为什么要用bufio?

开始之前我们先来看一段代码:

go 复制代码
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 读取当前目录 data.txt文件内容
	file, err := os.Open("./data.txt")
	if err != nil {
		fmt.Println("打开文件错误:", err)
		return
	}
	defer file.Close()

	data := make([]byte, 3)
	// 读取10次 每次读取3个字节
	for i := 0; i < 10; i++ {
		_, err := file.Read(data)

		// 遇到文件结束
		if err == io.EOF {
			fmt.Println(err)
			break
		}
		fmt.Println(string(data))
	}
}

上面实现了一个简单的文件读取功能,能正常工作,但是有一个有一个问题,每次从文件读取3个字节,而且读取了10次,也就是读取了3 * 10 = 30个字节的数据,却做了10次io操作,性能可想而知。

那么我们如何优化呢? 请出我们的主角bufio,它的主要作用是:减少io操作次数,提供读写性能

我们用bufio优化下

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	// 读取当前目录 data.txt文件内容
	file, err := os.Open("./data.txt")
	if err != nil {
		fmt.Println("打开文件错误:", err)
		return
	}
	defer file.Close()

	// 用bufio封装一层 返回一个reader
	reader := bufio.NewReader(file)

	data := make([]byte, 3)
	// 读取10次 每次读取3个字节
	for i := 0; i < 10; i++ {
		_, err := reader.Read(data) // 这里改成从reader中读

		// 遇到文件结束
		if err == io.EOF {
			fmt.Println(err)
			break
		}
		fmt.Println(string(data))
	}
}

优化很简单总共两步:

  1. bufio封装一层返回一个reader
  2. bufio.Reader去替换原来的直接文件(io.Reader)读

2. bufio缓冲区读写原理

首先bufio的主要对象是缓冲区,操作主要有两个:

记住,它底层的所有东西都围绕读、写展开。

原理上,我们也按照读、写来分别说明:

PS: 下面流程只是一个大概参考,不代表全部逻辑

lua 复制代码
 
 读取长度小于缓冲区大小,从缓冲区读取
 1.----------------->
                    当缓冲区为空,直接从文件读取,填满缓冲区
                    2. -------------->
 【程序】           【缓冲区】           【文件(io.Reader)】
  
  3. 读取长度超过缓冲区大小,直接从文件读取
  ----------------------------------> 
lua 复制代码
 写长度小于缓冲大小,先写入缓冲区
 1.----------------->
                    当缓冲区满,触发写入到文件
                    2. -------------->
 【程序】           【缓冲区】           【文件(io.Reader)】
  
  3. 写长度超过缓冲区大小,直接写入文件
  -----------------------------------> 

在bufio内部实现的reader和writer,大致是按照上述逻辑处理的,还有些细节的东西,没有在上面画出,但是做为初学者,了解下就行。

3. bufio读

在介绍之前,先说明一点,无论是读还是写,其构造过程都是差不多的:

  1. NewReader/NewWriter构造一个读/写对象
  2. 传入一个实现了io.Reader/io.Writer的对象

1. 构造bufio读对象

只要是实现了io.Reader对象都可以,比如:

go 复制代码
// =================1.从文件==============
file, err := os.Open("./data.txt")
if err != nil {
fmt.Println("打开文件错误:", err)
return
}
defer file.Close()

reader := bufio.NewReader(file)

// =================2. 从字符串=========
strReader := strings.NewReader("hello world")
bufio.NewReader(strReader)

// =================3. 从网络链接=======
bufio.NewReader(conn)

这里就不一一列举了。

2. Read读

和直接从原始对象读一样

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	strReader := strings.NewReader("hello world")
	buf := bufio.NewReader(strReader)

	// 读前要构造一个切片 用于存放读取的内容
	data := make([]byte, 5)
	// 读取数据到data
	_, err := buf.Read(data)

	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(data)) // 转字符串打印
}

// hello

3. ReadLine 按照行读取

有两点需要注意:

  1. 它返回三个参数 line、isPrefix、err
  2. 如果一行太长本次没读取完,则isPrefix会是true
  3. 返回的文本不包括行尾("\r\n"或"\n")

ps: 官方更推荐使用ReadString/ReadBytes/Scaner

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"io"
	"strings"
)

func main() {
	str := `
	 大家好
	 非常好
	 非常非常好
	`
	strReader := strings.NewReader(str)
	buf := bufio.NewReader(strReader)

	for {
		// 返回三个参数 line、是否前缀、错误
		line, _, err := buf.ReadLine()
		// 结束直接返回
		if err == io.EOF {
			fmt.Println("结束啦")
			break
		}

		// 字符串直接打印
		fmt.Println(string(line))
	}
}

// 大家好
// 非常好
// 非常非常好
// 结束啦

4. ReadString 直接读出字符串

它有两个好处:

  1. 直接返回字符串,省得转换
  2. 不用事先构造一个切片来装读取到的数据

注意它读取后的内容里是包含分割符号的

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"io"
	"strings"
)

func main() {
	str := `
	 大家好
	 非常好
	 非常非常好
	`
	strReader := strings.NewReader(str)
	buf := bufio.NewReader(strReader)

	for {
		// 这里是一个分割符
		s, err := buf.ReadString('\n')
		// 结束直接返回
		if err == io.EOF {
			fmt.Println("结束啦")
			break
		}

		// 字符串直接打印
		fmt.Printf(s)
	}
}

// 大家好
// 非常好
// 非常非常好
// 结束啦

这里还有几个类似的方法,非常接近,就不单独演示了 区别在于,ReadBytes 它返回一个字节切片([]byte)

5. Scanner 扫描

特点:

  1. 自己定义一个扫描函数,然后按照规则扫描;如果不指定扫描器,它和单独按照行读取类型;
  2. 返回内容不包含换行符
go 复制代码
package main

import (
	"bufio"
	"fmt"
	"strings"
)

func main() {
	str := `
	 大家好
	 非常好
	 非常非常好
	`
	strReader := strings.NewReader(str)
	// 先生成一个Scanner
	scanner := bufio.NewScanner(strReader)

	// 扫描每行
	for scanner.Scan() {
		// 返回的是一个字符串
		content := scanner.Text()
		fmt.Println(content)
	}

	// 检查扫描过程是否报错
	if err := scanner.Err(); err != nil {
		fmt.Println("扫描过程发生了错误:", err.Error())
	}
}

4. bufio 写

缓冲区默认大小为4K(4096字节) 这里需要注意的是,如果缓冲区没有满,不会自动写入io; 我们可以手动Flush 完成写入

先看下代码:

go 复制代码
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {

	// os.O_RDWR|os.O_CREATE 读写 如果不存在则创建
	file, err := os.OpenFile("data.txt", os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		fmt.Println(err)
		return
	}

	defer file.Close()
	// 构造缓冲写
	buf := bufio.NewWriter(file)

	// 三次write写入缓冲
	buf.Write([]byte("hello world\n"))
	buf.Write([]byte("非常美丽\n"))
	buf.Write([]byte("不错吧\n"))

	// 直接写入文件
	buf.Flush()
}

1. 构造writer

go 复制代码
//直接用io.Writer构造
buf := bufio.NewWriter(file)

// 指定缓冲大小 (最小是16字节)
buf := bufio.NewWriterSize(file, 30)

2. 各种wirter方式

主要有以下几种方式:

go 复制代码
// 以字符串方式写入
buf.WriteString("来吧来吧来\n")
	
// 一次写一个rune字符 返回实际占用的字节数
n, _ := buf.WriteRune('中')
c, _ := buf.WriteRune('\n')

// 一次写入一个byte
buf.WriteByte('a')
buf.WriteByte('A')

3. Flush写入io

go 复制代码
// 直接写入io
buf.Flush()

4. 其它

go 复制代码
// 重置buf 此前缓冲中的数据都被清理掉 
buf.Reset(os.Stdout)

// 缓冲区大小(总大小)
buf.Size()
// 缓冲区可用大小
buf.Available()
相关推荐
BlockChain8888 小时前
Solidity 实战【三】:重入攻击与防御(从 0 到 1 看懂 DAO 事件)
go·区块链
剩下了什么13 小时前
Gf命令行工具下载
go
地球没有花14 小时前
tw引发的对redis的深入了解
数据库·redis·缓存·go
BlockChain8881 天前
字符串最后一个单词的长度
算法·go
龙井茶Sky1 天前
通过higress AI统计插件学gjson表达式的分享
go·gjson·higress插件
宇宙帅猴2 天前
【Ubuntu踩坑及解决方案(一)】
linux·运维·ubuntu·go
SomeBottle3 天前
【小记】解决校园网中不同单播互通子网间 LocalSend 的发现问题
计算机网络·go·网络编程·学习笔记·计算机基础
且去填词3 天前
深入理解 GMP 模型:Go 高并发的基石
开发语言·后端·学习·算法·面试·golang·go
大厂技术总监下海4 天前
向量数据库“卷”向何方?从Milvus看“全功能、企业级”的未来
数据库·分布式·go·milvus·增强现实
冷冷的菜哥4 天前
go(golang)调用ffmpeg对视频进行截图、截取、增加水印
后端·golang·ffmpeg·go·音视频·水印截取截图