likes
comments
collection
share

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

作者站长头像
站长
· 阅读数 49

大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。

bufio 是 Go 语言中的一个包,主要用于提供带缓冲的 I/O 功能。它通过引入缓冲区来提高读取和写入操作的效率,尤其是在处理大量数据时。

Golang 中的 bufio 可以缓存数据,减少系统调用的次数,从而提升性能,是一个很常用的包。

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 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原来是空的,就直接写入文件系统中,不再缓存:

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

副作用及局限性

虽然 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 的源码:

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

代码的注释中清楚得说明了:

如果发生了一次 Write 错误,后续的 Write 都会失效,直接返回错误, Flush 也会错误。

看 Flush() 函数的源码:

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

那有没有办法重置这个错误呢?是可以的,但是有其他问题:

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

可以看到,err 被重置为 nil 的同时,缓存数据的标记 n 也被重置为了0,也就是写入的数据都被丢弃了。

倒掉洗澡水时候连孩子也一起倒掉了。

最终,我们去掉了超时的设置,来避免这个问题,但是隐患还是存在(理论上没有超时的错误还是可能出现其他错误)。

3. 消息延迟

bufio 使用缓冲区来延迟系统调用。这意味着在使用 bufio 时,数据不会立即被写入或读取到底层的 io.Writer 或 io.Reader。相反,数据首先存储在缓冲区中,然后在特定的条件下(如缓冲区满或调用 Flush 方法)才会进行实际的系统调用。这可以减少系统调用的次数,但也会增加延迟

如果你需要实时读写数据,而不关心系统调用次数,可能需要直接使用底层的 io.Writer 或 io.Reader。

有一些场景,比如对实时性要求比较高的应用:如动作类游戏,对于网络通信要求实时触达,那就不太适合用 bufio 来传输数据。

结尾

上面就是我们在使用 bufio 中总结出的经验和教训,希望对大家有点用处,如有问题大家可以一起在评论区探讨。

好了,看了这么多,一定很费脑力吧。来个笑话放松一下 :)

【笑话一则】篮球场上怎么能用一句话击垮对方?答:你的鞋是假的。

感谢您花时间阅读这篇文章!如果觉得有趣或有收获,请来个关注、评论、点赞吧,您的鼓励是我持续创作的动力,蟹蟹!

【Go探究】原来bufio还有这些门道大家好啊,我是码财同行。今天来聊聊 Go 语言中的 bufio。 bufio 是

转载自:https://juejin.cn/post/7413314085708677160
评论
请登录