【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是
大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。
bufio 是 Go 语言中的一个包,主要用于提供带缓冲的 I/O 功能。它通过引入缓冲区来提高读取和写入操作的效率,尤其是在处理大量数据时。
Golang 中的 bufio 可以缓存数据,减少系统调用的次数,从而提升性能,是一个很常用的包。
典型用法
bufio 的典型用法如下,我们打开一个文件,并使用缓存IO来写入一些数据,并通过 Flush 把数据刷入磁盘:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Create("file.txt")
if err != nil {
fmt.Println(err)
return
}
writer := bufio.NewWriter(file)
_, err = writer.Write([]byte("Hello World!"))
if err != nil {
fmt.Println(err)
return
}
err = writer.Flush()
if err != nil {
fmt.Println(err)
return
}
}
性能对比
我们来看一下,直接 write 和 利用 bufio 写入的性能对比结果:
goos: linux
goarch: amd64
cpu: Intel(R) Xeon(R) Gold 6348 CPU @ 2.60GHz
BenchmarkIO-16 964252 1204 ns/op 0 B/op 0 allocs/op
BenchmarkBufio-16 37086085 31.75 ns/op 16 B/op 1 allocs/op
PASS
ok command-line-arguments 2.396s
bufio 是直接 IO 的40倍左右,性能提升很明显。
一次写很多会怎么样?
可以想象,bufio 中肯定有个 buf 来缓存我们写入的数据,这个buf默认的大小是4096个 byte。
如果一次写入比这个buf 的长度还大的数据会怎么样?
因为默认的buf长度是 4096,因此超过buf长度的写入,肯定要先把buf填满,然后调用Flush 把已有数据刷新到磁盘之后再继续写入。这里 Go 还做了额外的优化:如果buf原来是空的,就直接写入文件系统中,不再缓存:
副作用及局限性
虽然 bufio 很好,但任何事物或者方案都有其局限性和副作用,了解了这一点我们才能更深入的理解 bufio。
在使用的时候要小心,否则就要踩坑。
那么,bufio 在使用的时候有哪些副作用或者局限性呢?下面来揭晓答案。
1. 并发的问题
bufio 底层实现时有一个 buf 存在,这个 buf 并没有加锁,因此多个协程并发读写时就会出问题,需要特别注意。
下面看一个例子。有很多协议包在设计的时候采用的是包头+包体
的二段式设计,在读取包的时候,因为并不知道需要读多长,因此先读取一个固定长度的包头,从中解出包长n,然后再继续读取长度为n的包体:
func unpack(reader bufio.Reader) {
// 读取包头
var h header
io.ReadFull(reader, h.buf[:])
// 从包头中取出包体长度
bodyLen := h.bodylen()
// 读取包体
payload := make([]byte, bodyLen)
io.ReadFull(reader, payload)
}
如果这段 unpack 的代码由多个协程并发执行,就会出问题:一个goroutine刚读完了包头,包体就被别的goroutine读走了,接下来数据就会错乱。
要解决这个问题,要么加锁,要么由一个goroutine读数据,再分发给多个goroutine处理逻辑。
2. 写入错误的问题
之前我们线上测试时,有一段往tcp连接写入数据的代码,如下:
conn.SetWriteDeadline(time.Now().Add(time.Second * 5))
err = connWriter.WritePkg(tc, pkg)
if err != nil {
logger.Error("handleWritePkg failed, client:%s, error:%v", addr, err)
}
其中 connWriter 是一个带 bufio 缓冲的 tcp 连接,每次写入数据时会设置设置写入超时。
在某个时间点突然发生写入超时错误。
Linux 服务器在运行时可能会发生时钟跳变的情况。这种情况可能由以下几种原因导致: 1. 硬件问题:服务器上的硬件时钟(CMOS 时钟)可能出现故障,导致时间不准确。2. 网络时间同步问题:如果服务器没有正确同步网络时间(NTP),或者 NTP 服务器出现故障,时间就可能发生跳变。3. 电源故障:短暂的电源中断或者不稳定的电源供应,也可能导致时钟跳变。4. 软件问题:一些软件bug、内核调整或者系统配置错误,也可能导致时钟跳变。
写入一次报错了可以理解,但是奇怪的是这个错误会一直报。
带着疑问我们看了 bufio 的源码:
代码的注释中清楚得说明了:
如果发生了一次 Write 错误,后续的 Write 都会失效,直接返回错误, Flush 也会错误。
看 Flush() 函数的源码:
那有没有办法重置这个错误呢?是可以的,但是有其他问题:
可以看到,err 被重置为 nil 的同时,缓存数据的标记 n 也被重置为了0,也就是写入的数据都被丢弃了。
倒掉洗澡水时候连孩子也一起倒掉了。
最终,我们去掉了超时的设置,来避免这个问题,但是隐患还是存在(理论上没有超时的错误还是可能出现其他错误)。
3. 消息延迟
bufio 使用缓冲区来延迟系统调用。这意味着在使用 bufio 时,数据不会立即被写入或读取到底层的 io.Writer 或 io.Reader。相反,数据首先存储在缓冲区中,然后在特定的条件下(如缓冲区满或调用 Flush 方法)才会进行实际的系统调用。这可以减少系统调用的次数,但也会增加延迟
。
如果你需要实时读写数据,而不关心系统调用次数,可能需要直接使用底层的 io.Writer 或 io.Reader。
有一些场景,比如对实时性要求比较高的应用:如动作类游戏,对于网络通信要求实时触达,那就不太适合用 bufio 来传输数据。
结尾
上面就是我们在使用 bufio 中总结出的经验和教训,希望对大家有点用处,如有问题大家可以一起在评论区探讨。
好了,看了这么多,一定很费脑力吧。来个笑话放松一下 :)
【笑话一则】篮球场上怎么能用一句话击垮对方?答:你的鞋是假的。
感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请来个关注、评论、点赞吧,您的鼓励是我持续创作的动力,蟹蟹!
转载自:https://juejin.cn/post/7413314085708677160