Go 陷阱之缓冲区内容不输出

概述 #

bufio 包实现了 缓冲 IO,它通过在内部封装一个 io.Readerio.Writer 来实现具体的读写操作。 通过 缓冲 IO 可以大大提升 IO 操作的性能,但是有时候,缓冲区也会带来一些违反直觉的问题。

下面的这个小案例是笔者在真实项目中遇到的,整理一下,分享给大家。

错误的做法 #

package main

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

func main() {
	_, err := fmt.Fprintf(os.Stdout, "%s\n", "[unbuffered] hello world")
	if err != nil {
		panic(err)
	}

	buf := bufio.NewWriter(os.Stdout)
	_, err = fmt.Fprintf(buf, "%s\n", "[buffered] hello world") // 不会输出
	if err != nil {
		panic(err)
	}
}

// $ go run main.go
// 输出如下 
/**
  [unbuffered] hello world
*/

通过输出的结果可以看到,缓冲区数据并没有刷出

Debug #

追踪一下源代码,看看问题出在哪里。

文件名称: $GOROOT/go/src/bufio/bufio.go

bufio.NewWriter 返回的对象为 bufio.Writer, 实现了标准库的 Write 接口:

func (b *Writer) Write(p []byte) (nn int, err error) {
	// 如果写入数据长度大于缓冲区可用数据长度
	for len(p) > b.Available() && b.err == nil {
		var n int
		if b.Buffered() == 0 {
			// 如果缓冲区未写入任何数据,直接写入
			n, b.err = b.wr.Write(p)
		} else {
			// 如果缓冲区存在写入的数据
			// 复制数据,然后调用 Flush
			n = copy(b.buf[b.n:], p)
			b.n += n
			b.Flush()
		}
		nn += n
		p = p[n:]
	}
	// 发生了错误
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], p)
	b.n += n
	nn += n
	return nn, nil
}

// 返回缓冲区可用数据长度
func (b *Writer) Available() int { return len(b.buf) - b.n }

// 返回已写入缓冲区的长度
func (b *Writer) Buffered() int { return b.n }

错误的原因在于: 缓冲区的默认长度是 4096, 我们写入的数据太少了,远低于剩余缓冲区, 所以没有输出。

正确的做法 #

为了解决这个问题,我们可以模仿 Write 方法内部实现的写法,写入数据完成后,主动调用 Flush 方法刷出缓冲区。

package main

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

func main() {
	_, err := fmt.Fprintf(os.Stdout, "%s\n", "[unbuffered] hello world")
	if err != nil {
		panic(err)
	}

	buf := bufio.NewWriter(os.Stdout)
	_, err = fmt.Fprintf(buf, "%s\n", "[buffered] hello world")
	if err != nil {
		panic(err)
	}

	err = buf.Flush()
	if err != nil {
		panic(err)
	}
}

// $ go run main.go
// 输出如下 
/**
  [unbuffered] hello world
  [buffered] hello world
*/

通过输出的结果可以看到,缓冲区数据已经可以正常刷出了

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。

© 蛮荆 | 陕公网安备 61011302001681 号 | 陕ICP备2023004378号-1 | Rendered by Hugo