golang-bufio 缓冲写

news2024/11/28 18:52:36

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 error
	buf []byte
	n   int
	wr  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 main

import (
	"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 += 1
	fmt.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 -= n
		b.err = err
		return err
	}
    // 写入成功,把缓冲区置空(即 n 设置为 0)
	b.n = 0
	return 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 int
		if 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 += 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
}

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] = c
	b.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 += size
	return 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.StringWriter
	tryStringWriter := true

	nn := 0
	for len(s) > b.Available() && b.err == nil {
		var n int
		if 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 += n
			b.Flush()
		}
		nn += n
		s = s[n:]
	}
	if b.err != nil {
		return nn, b.err
	}
	n := copy(b.buf[b.n:], s)
	b.n += n
	nn += n
	return 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 int
	for {
        // 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 = err
			n += nn
			return n, err
		}
        // tip:读取传入的 reader 的数据,写入底层数据源,这里最大尝试100次失败
		nr := 0
		for 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 += m
		n += 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.coloradmin.cn/o/968070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

5.0: Dubbo服务导出源码解析

#Dubbo# 文章内容 Dubbo服务导出基本原理分析Dubbo服务注册流程源码分析Dubbo服务暴露流程源码分析服务导出的入口方法为ServiceBean.export(),此方法会调用ServiceConfig.export()方法,进行真正的服务导出。 1. 服务导出大概原理 服务导出的入口方法为ServiceBean.export…

stm32之28.ADC

须看原理图&#xff08;引脚、电压值、ADC几号通道&#xff09;配置 。 若对比值0~4096 模拟电压/参考电压4096/x 假设模拟电压2.1V&#xff0c;参考电压3.3v&#xff0c;4096/x3.3/2.1 ->3.3x2.1x4096 ->x2,606.5 也可反推出模拟电压 ADC转换时间 ADC时钟来源于…

leetcode645. 错误的集合(java)

错误的集合 题目描述优化空间代码演示 题目描述 难度 - 简单 LC645 - 错误的集合 集合 s 包含从 1 到 n 的整数。不幸的是&#xff0c;因为数据错误&#xff0c;导致集合里面某一个数字复制了成了集合里面的另外一个数字的值&#xff0c;导致集合 丢失了一个数字 并且 有一个数…

时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测(风电功率预测)

时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测&#xff08;风电功率预测&#xff09; 目录 时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测&#xff08;风电功率预测&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.时序预测 | MA…

Python中的绝对和相对导入

在本文中&#xff0c;我们将看到Python中的绝对和相对导入。 Python中导入的工作 Python中的import类似于C/C中的#include header_file。Python模块可以通过使用import导入文件/函数来访问其他模块的代码。import语句是调用import机制的最常见方式&#xff0c;但它不是唯一的…

常见数学名词的物理意义(更新中)

BetterExplained – Math lessons that click 一、复数 i 的物理意义就是旋转&#xff0c;i 就是逆时针旋转90&#xff0c;i*i 就是逆时针旋转180 加法&#xff1a;实部相加&#xff0c;虚部相加 乘法&#xff1a; 复数zxyi控制了函数的放缩和旋转 ——x 放缩&#xff0c;…

Shell开发实践:服务器的磁盘、CPU、内存的占用监控

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

基于vue-cli创建后台管理系统前端页面——element-ui,axios,跨域配置,布局初步,导航栏

目录 引出安装npm install安装element-ui安装axios 进行配置main.js中引入添加jwt前端跨域配置 进行初始布局HomeView.vueApp.vue 新增页面和引入home页面导航栏总结 引出 1.vue-cli创建前端工程&#xff0c;安装element-ui&#xff0c;axios和配置&#xff1b; 2.前端跨域的配…

SQL查询本年每月的数据

--一、以一行数据的形式&#xff0c;显示本年的12月的数据&#xff0c;本示例以2017年为例&#xff0c;根据统计日期字段判断&#xff0c;计算总和&#xff0c;查询语句如下&#xff1a;selectsum(case when datepart(month,统计日期)1 then 支付金额 else 0 end) as 1月, sum…

UE4 春节鞭炮

先搞个基类&#xff0c;一个鞭炮的 搞个鞭炮类&#xff0c;存多个鞭炮 在构造函数的位置先生成对应的鞭炮数 将鞭炮绑定到绳子上&#xff0c;随绳子摆动而一起摆动 在基类里面写爆炸事件 最后用Timer去调用

一篇文章搞懂Redis缓存

目录 一、什么是缓存缓存的优缺点缓存的优点缓存的缺点 二、Redis缓存三、缓存的更新策略主动更新策略 四、缓存穿透解决方案 五、缓存雪崩解决方案 六、缓存击穿解决方案 一、什么是缓存 我们都知道在计算机中内存的速度比磁盘要快非常多&#xff0c;如果每次都要去磁盘获取数…

《Python魔法大冒险》004第一个魔法程序

在图书馆的一个安静的角落,魔法师和小鱼坐在一张巨大的桌子前。桌子上摆放着那台神秘的笔记本电脑。 魔法师: 小鱼,你已经学会了如何安装魔法解释器和代码编辑器。是时候开始编写你的第一个Python魔法程序了! 小鱼:(兴奋地两眼放光)我准备好了! 魔法师: 不用担心,…

Lesson6---案例:人脸案例

学习目标 了解opencv进行人脸检测的流程了解Haar特征分类器的内容 1 基础 我们使用机器学习的方法完成人脸检测&#xff0c;首先需要大量的正样本图像&#xff08;面部图像&#xff09;和负样本图像&#xff08;不含面部的图像&#xff09;来训练分类器。我们需要从其中提取特…

解决Debian系统通过cifs挂载smb后,中文目录乱码问题

解决Debian系统通过cifs挂载smb后&#xff0c;中文目录乱码问题 //$smb_server/share /mnt/nas_share cifs credentials/root/.smbcredentials,iocharsetutf8 0 0默认通过以上命令挂载smb&#xff0c;但是在查看文件目录时&#xff0c;中文乱码 解决问题方式&#xff1a; de…

BLDC无感方波控制

BLDC无感控制 反电动势过零检测反电动势检测方法比较器模式采样过零信号闭环的建立 BLDC 方波启动技术转子预定位电机的外同步加速电机运行状态的转换 程序部分 反电动势过零检测 它的主要核心就是通过检测定子绕组的反电动势过零点来判断转子当前的位置。 三相六状态 120通电…

JavaScript -【第二周】

文章来源于网上收集和自己原创&#xff0c;若侵害到您的权利&#xff0c;请您及时联系并删除~~~ 理解什么是流程控制&#xff0c;知道条件控制的种类并掌握其对应的语法规则&#xff0c;具备利用循环编写简易ATM取款机程序能力 运算符语句综合案例 1. 运算符 算术运算符赋值运…

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

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

简明易懂:Python中的分支与循环

文章目录 前言分支结构if 语句&#xff1a;单一条件判断else语句&#xff1a;提供备选方案elif 语句&#xff1a;多条件判断嵌套的分支结构&#xff1a;复杂条件逻辑 循环结构for循环&#xff1a;遍历序列range()函数与for循环while循环&#xff1a;条件重复循环控制&#xff1…

C++------vector【STL】

文章目录 vector的介绍及使用vector的介绍vector的使用 vector的模拟实现 vector的介绍及使用 vector的介绍 1、vector是表示可变大小数组的序列容器。 2、就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问和数…

【rust/egui】(八)使用panels给你的应用划分功能区块

说在前面 rust新手&#xff0c;egui没啥找到啥教程&#xff0c;这里自己记录下学习过程环境&#xff1a;windows11 22H2rust版本&#xff1a;rustc 1.71.1egui版本&#xff1a;0.22.0eframe版本&#xff1a;0.22.0上一篇&#xff1a;这里 panel是啥 panel是ui上的一块区域&…