golang-bufio 缓冲写

1. 缓冲写

在阅读这篇博客之前,请先阅读上一篇:golang-bufio 缓冲读

// buffered output// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.
type Writer struct {err errorbuf []byten   intwr  io.Writer
}

缓冲写比缓冲读取更加简单,它的工作原来就是当缓冲区满(或者用户手动强制刷新)了,把整个缓冲区中的内容写入底层数据源。它提供了两种创建的方法:

  • func NewWriterSize(w io.Writer, size int) *Writer 创建一个指定缓冲区大小的缓冲 Writer 并返回。
  • func NewWriter(w io.Writer) *Writer 创建默认缓冲区大小的缓冲 Writer 并返回。

注意:

  1. 缓冲 Reader 的默认缓冲区大小是 4096,最小是 16,如果你设置低于这个值,它会强制设置成 16。
  2. 缓冲 Writer 的默认缓冲区大小是 4096,但是没有最小值,所以你可以设置的很小,但是太小了会有问题,例如当你调用写入单个字符时。

2. 缓冲写测试

这里在正式介绍之前,我们先来使用一下它,不过这里只是演示一下缓冲写的作用。缓冲写其实就是延迟写入,所以它减少的是真正写入的次数(向磁盘文件写入或者写入网络流代价都是很高的)。下面的示例可以看到,不使用缓冲写入,调用一次写入就会实际写入一次。使用缓冲写入后,写满缓冲区或者手动调用 Flush() 才会实际写入。

package mainimport ("bufio""fmt""strings"
)func main() {BufioWriterTest()
}func BufioWriterTest() {mBlock := NewMemoryBlock()// 直接写入,不使用缓冲流,调用几次就是写入几次。mBlock.Write([]byte("I love you yesterday and today."))mBlock.Write([]byte("I love you yesterday and today."))mBlock.Write([]byte("I love you yesterday and today."))mBlock.Write([]byte("I love you yesterday and today."))mBlock.Write([]byte("I love you yesterday and today."))fmt.Printf("查看 MemoryBlock 中的内容:%s\n", mBlock.String())bufWriter := bufio.NewWriter(mBlock)// 使用缓冲流,写满缓冲区或者手动调用 Flush() 才会实际写入。bufWriter.WriteString("I love you yesterday and today.")bufWriter.WriteString("I love you yesterday and today.")bufWriter.WriteString("I love you yesterday and today.")bufWriter.WriteString("I love you yesterday and today.")bufWriter.WriteString("I love you yesterday and today.")fmt.Printf("查看 MemoryBlock 中的内容:%s\n", mBlock.String())bufWriter.Flush()fmt.Printf("查看 MemoryBlock 中的内容:%s\n", mBlock.String())}// 实现一个简陋的 io.Writer,俄罗斯套娃
// 它只是简单的把写入的切片附加到原来的切片上。
type MemoryBlock struct {data []byte // 使用切片来存储数据n    int    // 写入次数
}func NewMemoryBlock() *MemoryBlock {return &MemoryBlock{data: make([]byte, 0)}
}// 写入数据
func (dw *MemoryBlock) Write(p []byte) (int, error) {// 每次写入 dw.n 次数加一dw.n += 1fmt.Printf("MemoryBlock has written %d times\n", dw.n)dw.data = append(dw.data, p...)return len(p), nil
}// 查看内部数据
func (dw *MemoryBlock) String() string {return string(dw.data)
}

在这里插入图片描述

3. 主要方法介绍

接下来会介绍缓冲写的主要方法的作用,并且会添加一些个人的注释。如果有不对的地方,欢迎指正。

3.1 刷新缓冲区 Flush()

缓冲 Writer 的写入都不是真的将数据写入底层数据源,而是写入缓冲区,真正写入靠的是 Flush() 方法。如果缓冲区满了,也是调用 Flush() 来写入的(清空缓冲区)。理解了这个方法,你就大概了解缓冲写入的原理了。

// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {// tip:如果有错误就返回,不再写入if b.err != nil {return b.err}// tip:如果缓冲区是空的,不写入,直接返回if b.n == 0 {return nil}// tip:把缓冲区内的所有数据一次性写入底层数据源n, err := b.wr.Write(b.buf[0:b.n])// tip:只写入部分数据,且错误为空(这应该很少发生的)if n < b.n && err == nil {err = io.ErrShortWrite}// tip:处理写入发生的错误if err != nil {// tip:只写入部分数据(写入错误不为空)if n > 0 && n < b.n {// 把未写入的数据复制到缓冲区的开头copy(b.buf[0:b.n-n], b.buf[n:b.n])}// 缓冲区缓冲字节数减去已经写入的数目b.n -= nb.err = errreturn err}// 写入成功,把缓冲区置空(即 n 设置为 0)b.n = 0return nil
}

3.2 可用缓冲区 AvailableBuffer()

这个方法返回一个容量为可用缓冲区大小的空切片,我有点不理解它的作用是什么。这里看注释是说,这个空切片是打算用来追加数据(append),然后传递给一个立即连续的写入 (Write)调用。并且,它只在下一次写入操作之前有效(因为写入会影响切片的内容)。

// AvailableBuffer returns an empty buffer with b.Available() capacity.
// This buffer is intended to be appended to and
// passed to an immediately succeeding Write call.
// The buffer is only valid until the next write operation on b.
func (b *Writer) AvailableBuffer() []byte {return b.buf[b.n:][:0]
}

3.3 缓冲区可用字节数 Available()

这个方法虽然很简单,但是后面会经常用到它,所以这里也提一下。它的功能很简单,返回缓冲区中的可用字节数(还能写入多少数据),即缓冲区大小 - 已经写入的字节数

// Available returns how many bytes are unused in the buffer.
func (b *Writer) Available() int { return len(b.buf) - b.n }

3.4 写入字节切片 Write(p []byte)

如果需要写入的数据超过了缓冲区的剩余大小且没有错误则执行循环:

如果缓冲区已经缓冲的数据为 0,即空的缓冲区,那么直接写入底层数据源(先缓冲再写入就浪费时间了)。否则,把需要写入的数据复制到缓冲区后面,然后调用一次 Flush() 进行刷新 n = copy(b.buf[b.n:], p)。然后,累加实际写入的字节数,同时更新待写入的切片(这里就体现了切片的灵活性!)。

如果切片中数据加上缓冲区中的数据仍然不满一个缓冲区,只是把数据加入缓冲区中,并不实际写入。这就是缓冲的作用了,通过延迟写入来提高性能(但是牺牲了实时性)。

// Write writes the contents of p into the buffer.
// It returns the number of bytes written.
// If nn < len(p), it also returns an error explaining
// why the write is short.
func (b *Writer) Write(p []byte) (nn int, err error) {for len(p) > b.Available() && b.err == nil {var n intif b.Buffered() == 0 {// Large write, empty buffer.// Write directly from p to avoid copy.n, b.err = b.wr.Write(p)} else {n = copy(b.buf[b.n:], p)b.n += nb.Flush()}nn += np = p[n:]}if b.err != nil {return nn, b.err}n := copy(b.buf[b.n:], p)b.n += nnn += nreturn nn, nil
}

3.5 写入单个字节 WriteByte(c byte)

// WriteByte writes a single byte.
func (b *Writer) WriteByte(c byte) error {if b.err != nil {return b.err}// tip:如果缓冲区满了(不过这里应该不会小于 0 吧?),// 它会调用 Flush() 强制写入(会处理错误)if b.Available() <= 0 && b.Flush() != nil {return b.err}// tip:缓冲区还有足够的大小可以写入,直接把它写入缓冲区b.buf[b.n] = cb.n++return nil
}

3.6 写入单个字符 WriteRune(r rune)

这个方法是写入单个字符(多个字节)的,它基本和写入单个字节是一样的,不过这里需要把字符作为一个整体考虑。主要的区别在于,如果缓冲区可用字节数小于 utf8 的最大字节数(4字节),它会强制刷新,然后再把字符写入缓冲区。也就是说,它不会把一个 rune 拆分成多个字节发送,而是一次发送整个的字符,至于原因可能是分开发送会导致接收端乱码。

有一个比较有意思的地方,如果强制刷新之后,缓冲区的可用字节数还是 utf8 的最大字节数呢?此时缓冲区是空的,说明整个缓冲区的大小小于 4!官方也吐槽了一句:Can only happen if buffer is silly small.

// WriteRune writes a single Unicode code point, returning
// the number of bytes written and any error.
func (b *Writer) WriteRune(r rune) (size int, err error) {// Compare as uint32 to correctly handle negative runes.if uint32(r) < utf8.RuneSelf {err = b.WriteByte(byte(r))if err != nil {return 0, err}return 1, nil}if b.err != nil {return 0, b.err}n := b.Available()if n < utf8.UTFMax {if b.Flush(); b.err != nil {return 0, b.err}n = b.Available()if n < utf8.UTFMax {// tip:这是哪个傻子设置的小缓冲区!// Can only happen if buffer is silly small.return b.WriteString(string(r))}}size = utf8.EncodeRune(b.buf[b.n:], r)b.n += sizereturn size, nil
}

3.7 写入字符串 WriteString(s string)

写入一个字符串,并不会把整个字符串的内容都写入底层数据源。如果字符串很大(超过了缓冲区的大小)且缓冲区是空的,那么它会直接写入底层数据源,不会先写入缓冲区再写入底层数据源(一次能完成的时候,当然不需要做多次了)。否则,就是将字符串内容填满缓冲区,然后每次写入一整个缓冲区,知道最后的内容不满一个缓冲区。这些内容就留在缓冲区中了,不会写入底层数据源,直到下一次写满缓冲区或者强制刷新 Flush()

// WriteString writes a string.
// It returns the number of bytes written.
// If the count is less than len(s), it also returns an error explaining
// why the write is short.
func (b *Writer) WriteString(s string) (int, error) {var sw io.StringWritertryStringWriter := truenn := 0for len(s) > b.Available() && b.err == nil {var n intif b.Buffered() == 0 && sw == nil && tryStringWriter {// Check at most once whether b.wr is a StringWriter.sw, tryStringWriter = b.wr.(io.StringWriter)}if b.Buffered() == 0 && tryStringWriter {// Large write, empty buffer, and the underlying writer supports// WriteString: forward the write to the underlying StringWriter.// This avoids an extra copy.n, b.err = sw.WriteString(s)} else {n = copy(b.buf[b.n:], s)b.n += nb.Flush()}nn += ns = s[n:]}if b.err != nil {return nn, b.err}n := copy(b.buf[b.n:], s)b.n += nnn += nreturn nn, nil
}

3.8 写入其他数据源 ReadFrom(r io.Reader)

这个方法,我就把它叫做写入其他数据源了。它的作用就是直接写入一个数据源的数据,而不是先读取再写入(底层还是要读取再写入的,只不过提供了一个更易用的方法)。不过,这个方法的逻辑还是蛮复杂的,直接看注释吧。

// ReadFrom implements io.ReaderFrom. If the underlying writer
// supports the ReadFrom method, this calls the underlying ReadFrom.
// If there is buffered data and an underlying ReadFrom, this fills
// the buffer and writes it before calling ReadFrom.
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error) {if b.err != nil {return 0, b.err}// 把底层数据源转成 io.ReaderFrom,看其是否实现该接口readerFrom, readerFromOK := b.wr.(io.ReaderFrom)var m intfor {// tip:缓冲区满if b.Available() == 0 {if err1 := b.Flush(); err1 != nil {return n, err1}}// tip:缓冲区空,直接让其写入(不写缓冲区了)if readerFromOK && b.Buffered() == 0 {nn, err := readerFrom.ReadFrom(r)b.err = errn += nnreturn n, err}// tip:读取传入的 reader 的数据,写入底层数据源,这里最大尝试100次失败nr := 0for nr < maxConsecutiveEmptyReads {m, err = r.Read(b.buf[b.n:])if m != 0 || err != nil {break}nr++}if nr == maxConsecutiveEmptyReads {return n, io.ErrNoProgress}b.n += mn += int64(m)if err != nil {break}}// 如果读取发生的错误是 io.EOF,这是正常情况,否则返回错误情况。// 如果缓冲区正好满了,那么把数据写入底层数据源,否则只是把数据写入缓冲区。if err == io.EOF {// If we filled the buffer exactly, flush preemptively.if b.Available() == 0 {err = b.Flush()} else {err = nil}}return n, err
}

4. 缓冲输入和输出

看到最后面,发现还有一个同时处理缓冲读写的结构体,不过这个就是把前面的缓冲 Reader 和 缓冲 Writer 结合起来了,只提供了一个创建的的方法:func NewReadWriter(r *Reader, w *Writer) *ReadWriter。读和写的方法就是前面已经介绍过的了。

// buffered input and output// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {*Reader*Writer
}// NewReadWriter allocates a new ReadWriter that dispatches to r and w.
func NewReadWriter(r *Reader, w *Writer) *ReadWriter {return &ReadWriter{r, w}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/95659.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python爬虫】批量识别pdf中的英文,自动翻译成中文上

不管是上学还是上班,有时不可避免需要看英文文章,特别是在写毕业论文的时候。比较头疼的是把专业性很强的英文pdf文章翻译成中文。我记得我上学的时候,是一段一段复制,或者碰到不认识的单词就百度翻译一下,非常耗费时间。本文提供批量识别pdf中英文的方法,后续文章实现自…

无涯教程-Android - RadioGroup函数

RadioGroup类用于单选按钮集。 如果我们选中属于某个单选按钮组的一个单选按钮,它将自动取消选中同一组中以前选中的任何单选按钮。 RadioGroup属性 以下是与RadioGroup控制相关的重要属性。您可以查看Android官方文档以获取属性的完整列表以及可以在运行时更改这些属性的相关…

verilator——牛刀小试

verilator——牛刀小试 安装verilator可见&#xff1a;https://blog.csdn.net/qq_40676869/article/details/132648522?spm1001.2014.3001.5501 正文开始 编写一个异或的电路模块如下&#xff1a; top.v module top(input a,input b,output f );assign f a ^ b; endmodul…

Python3 条件控制

Python3 条件控制 Python 条件语句是通过一条或多条语句的执行结果&#xff08;True 或者 False&#xff09;来决定执行的代码块。 可以通过下图来简单了解条件语句的执行过程: 代码执行过程&#xff1a; if 语句 Python中if语句的一般形式如下所示&#xff1a; if conditi…

c++入门一

参考&#xff1a;https://www.learncpp.com/cpp-tutorial/ When you finish, you will not only know how to program in C, you will know how NOT to program in C, which is arguably as important. Tired or unhappy programmers make mistakes, and debugging code tends…

离散数据编码方式总结(OneHotEncoder、LabelEncoder、OrdinalEncoder、get_dummies、DictVector

写在前面 在机器学习的特征选择的时候&#xff0c;往往有一些离散的特征不好计算&#xff0c;此时需要对这些特征进行编码&#xff0c;但是编码方式有很多&#xff0c;不同的包也会有不同的编码方式。&#xff08;明白OneHotEncoder、LabelEncoder、OrdinalEncoder、get_dummi…

基于Stable Diffusion的AIGC服饰穿搭实践

本文主要介绍了基于Stable Diffusion技术的虚拟穿搭试衣的研究探索工作。文章展示了使用LoRA、ControlNet、Inpainting、SAM等工具的方法和处理流程&#xff0c;并陈述了部分目前的实践结果。通过阅读这篇文章&#xff0c;读者可以了解到如何运用Stable Diffusion进行实际操作&…

centos安装nginx实操记录(加安全配置)

1.下载与安装 yum -y install nginx2.启动命令 /usr/sbin/nginx -c /etc/nginx/nginx.conf3.新建配置文件 cd /etc/nginx/conf.d vim index.conf配了一个负责均衡&#xff0c;如不需要&#xff0c;可将 server localhost: 多余的去掉 upstream web_server{server localhost…

android 实现本地一键打包,告别繁琐的studio操作

前言 在实际开发项目中&#xff0c;我们的工程目录往往是多个app在一个工程下的&#xff0c;每次打包都需要手动的用studio点击Build->Generate Signed Bundle or APK->APK 选择app&#xff0c;签名等&#xff0c;甚至有的app签名还不一样&#xff0c;还需要手动的来回切…

Vue中如何为Echarts统计图设置数据

在前端界面接收后端数据后&#xff0c;将数据赋值给ECharts中的data时出现了&#xff0c;数据读取失败的问题&#xff08;可能是由于数据渲染的前后顺序问题&#xff09;。后通过如下方式进行了解决&#xff1a; 1、接下来将介绍UserController中的countUsers方法&#xff0c;…

【数据结构】二叉树篇|超清晰图解和详解:二叉树的序列化和反序列化

博主简介&#xff1a;努力学习的22级计算机科学与技术本科生一枚&#x1f338;博主主页&#xff1a; 是瑶瑶子啦每日一言&#x1f33c;: 你不能要求一片海洋&#xff0c;没有风暴&#xff0c;那不是海洋&#xff0c;是泥塘——毕淑敏 目录 一、核心二、题目2.1:前序遍历2.2&…

优思学院|六西格玛中的概率分布有哪些?

为什么概率分布重要&#xff1f; 概率分布是统计学中一个重要的概念&#xff0c;它帮助我们理解随机变量的分布情况以及与之相关的概率。在面对具体问题时&#xff0c;了解概率分布可以帮助我们选择适当的检验或分析策略&#xff0c;以解决问题并做出合理的决策。 常见的概率…