Go语言核心36讲41
你好,我是郝林,今天我们继续分享bytes包与字节串操作的相关内容。
在上一篇文章中,我们分享了bytes.Buffer
中已读计数的大致功用,并围绕着这个问题做了解析,下面我们来进行相关的知识扩展。
知识扩展
问题 1:bytes.Buffer
的扩容策略是怎样的?
Buffer
值既可以被手动扩容,也可以进行自动扩容。并且,这两种扩容方式的策略是基本一致的。所以,除非我们完全确定后续内容所需的字节数,否则让Buffer
值自动去扩容就好了。
在扩容的时候,Buffer
值中相应的代码(以下简称扩容代码)会先判断内容容器的剩余容量,是否可以满足调用方的要求,或者是否足够容纳新的内容。
如果可以,那么扩容代码会在当前的内容容器之上,进行长度扩充。
更具体地说,如果内容容器的容量与其长度的差,大于或等于另需的字节数,那么扩容代码就会通过切片操作对原有的内容容器的长度进行扩充,就像下面这样:
b.buf = b.buf[:length+need]
反之,如果内容容器的剩余容量不够了,那么扩容代码可能就会用新的内容容器去替代原有的内容容器,从而实现扩容。
不过,这里还有一步优化。
如果当前内容容器的容量的一半,仍然大于或等于其现有长度(即未读字节数)再加上另需的字节数的和,即:
cap(b.buf)/2 >= b.Len() + need
那么,扩容代码就会复用现有的内容容器,并把容器中的未读内容拷贝到它的头部位置。
这也意味着其中的已读内容,将会全部被未读内容和之后的新内容覆盖掉。
这样的复用预计可以至少节省掉一次后续的扩容所带来的内存分配,以及若干字节的拷贝。
若这一步优化未能达成,也就是说,当前内容容器的容量小于新长度的二倍。
那么,扩容代码就只能再创建一个新的内容容器,并把原有容器中的未读内容拷贝进去,最后再用新的容器替换掉原有的容器。这个新容器的容量将会等于原有容量的二倍再加上另需字节数的和。
新容器的容量=2*原有容量+所需字节数
通过上面这些步骤,对内容容器的扩充基本上就完成了。不过,为了内部数据的一致性,以及避免原有的已读内容可能造成的数据混乱,扩容代码还会把已读计数置为0
,并再对内容容器做一下切片操作,以掩盖掉原有的已读内容。
顺便说一下,对于处在零值状态的Buffer
值来说,如果第一次扩容时的另需字节数不大于64
,那么该值就会基于一个预先定义好的、长度为64
的字节数组来创建内容容器。
在这种情况下,这个内容容器的容量就是64
。这样做的目的是为了让Buffer
值在刚被真正使用的时候就可以快速地做好准备。
问题2:bytes.Buffer
中的哪些方法可能会造成内容的泄露?
首先明确一点,什么叫内容泄露?这里所说的内容泄露是指,使用Buffer
值的一方通过某种非标准的(或者说不正式的)方式,得到了本不该得到的内容。
比如说,我通过调用Buffer
值的某个用于读取内容的方法,得到了一部分未读内容。我应该,也只应该通过这个方法的结果值,拿到在那一时刻Buffer
值中的未读内容。
但是,在这个Buffer
值又有了一些新内容之后,我却可以通过当时得到的结果值,直接获得新的内容,而不需要再次调用相应的方法。
这就是典型的非标准读取方式。这种读取方式是不应该存在的,即使存在,我们也不应该使用。因为它是在无意中(或者说一不小心)暴露出来的,其行为很可能是不稳定的。
在bytes.Buffer
中,Bytes
方法和Next
方法都可能会造成内容的泄露。原因在于,它们都把基于内容容器的切片直接返回给了方法的调用方。
我们都知道,通过切片,我们可以直接访问和操纵它的底层数组。不论这个切片是基于某个数组得来的,还是通过对另一个切片做切片操作获得的,都是如此。
在这里,Bytes
方法和Next
方法返回的字节切片,都是通过对内容容器做切片操作得到的。也就是说,它们与内容容器共用了同一个底层数组,起码在一段时期之内是这样的。
以Bytes
方法为例。它会返回在调用那一刻其所属值中的所有未读内容。示例代码如下:
contents := "ab"
buffer1 := bytes.NewBufferString(contents)
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
contents, buffer1.Cap()) // 内容容器的容量为:8。
unreadBytes := buffer1.Bytes()
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 未读内容为:[97 98]。
我用字符串值"ab"
初始化了一个Buffer
值,由变量buffer1
代表,并打印了当时该值的一些状态。
你可能会有疑惑,我只在这个Buffer
值中放入了一个长度为2
的字符串值,但为什么该值的容量却变为了8
。
虽然这与我们当前的主题无关,但是我可以提示你一下:你可以去阅读runtime
包中一个名叫stringtoslicebyte
的函数,答案就在其中。
接着说buffer1
。我又向该值写入了字符串值"cdefg"
,此时,其容量仍然是8
。我在前面通过调用buffer1
的Bytes
方法得到的结果值unreadBytes
,包含了在那时其中的所有未读内容。
但是,由于这个结果值与buffer1
的内容容器在此时还共用着同一个底层数组,所以,我只需通过简单的再切片操作,就可以利用这个结果值拿到buffer1
在此时的所有未读内容。如此一来,buffer1
的新内容就被泄露出来了。
buffer1.WriteString("cdefg")
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap()) // 内容容器的容量仍为:8。
unreadBytes = unreadBytes[:cap(unreadBytes)]
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes) // 基于前面获取到的结果值可得,未读内容为:[97 98 99 100 101 102 103 0]。
如果我当时把unreadBytes
的值传到了外界,那么外界就可以通过该值操纵buffer1
的内容了,就像下面这样:
unreadBytes[len(unreadBytes)-2] = byte('X') // 'X'的ASCII编码为88。
fmt.Printf("The unread bytes of the buffer: %v\n", buffer1.Bytes()) // 未读内容变为了:[97 98 99 100 101 102 88]。
现在,你应该能够体会到,这里的内容泄露可能造成的严重后果了吧?对于Buffer
值的Next
方法,也存在相同的问题。
不过,如果经过扩容,Buffer
值的内容容器或者它的底层数组被重新设定了,那么之前的内容泄露问题就无法再进一步发展了。我在demo80.go文件中写了一个比较完整的示例,你可以去看一看,并揣摩一下。
总结
我们结合两篇内容总结一下。与strings.Builder
类型不同,bytes.Buffer
不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序地读取其中的子序列。
bytes.Buffer
类型使用字节切片作为其内容容器,并且会用一个字段实时地记录已读字节的计数。
虽然我们无法直接计算出这个已读计数,但是由于它在Buffer
值中起到的作用非常关键,所以我们很有必要去理解它。
无论是读取、写入、截断、导出还是重置,已读计数都是功能实现中的重要一环。
与strings.Builder
类型的值一样,Buffer
值既可以被手动扩容,也可以进行自动的扩容。除非我们完全确定后续内容所需的字节数,否则让Buffer
值自动去扩容就好了。
Buffer
值的扩容方法并不一定会为了获得更大的容量,替换掉现有的内容容器,而是先会本着尽量减少内存分配和内容拷贝的原则,对当前的内容容器进行重用。并且,只有在容量实在无法满足要求的时候,它才会去创建新的内容容器。
此外,你可能并没有想到,Buffer
值的某些方法可能会造成内容的泄露。这主要是由于这些方法返回的结果值,在一段时期内会与其所属值的内容容器共用同一个底层数组。
如果我们有意或无意地把这些结果值传到了外界,那么外界就有可能通过它们操纵相关联Buffer
值的内容。
这属于很严重的数据安全问题。我们一定要避免这种情况的发生。最彻底的做法是,在传出切片这类值之前要做好隔离。比如,先对它们进行深度拷贝,然后再把副本传出去。
思考题
今天的思考题是:对比strings.Builder
和bytes.Buffer
的String
方法,并判断哪一个更高效?原因是什么?
Go语言核心36讲41的更多相关文章
- Go语言核心36讲(Go语言实战与应用三)--学习笔记
25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...
- Go语言核心36讲(导读)--学习笔记
目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品 ...
- Go语言核心36讲(Go语言进阶技术八)--学习笔记
14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...
- Go语言核心36讲(Go语言进阶技术十六)--学习笔记
22 | panic函数.recover函数以及defer语句(下) 我在前一篇文章提到过这样一个说法,panic 之中可以包含一个值,用于简要解释引发此 panic 的原因. 如果一个 panic ...
- Go语言核心36讲(Go语言实战与应用一)--学习笔记
23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...
- Go语言核心36讲(Go语言实战与应用四)--学习笔记
26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...
- Go语言核心36讲(Go语言实战与应用十四)--学习笔记
36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...
- Go语言核心36讲(Go语言实战与应用十八)--学习笔记
40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...
- Go语言核心36讲(Go语言实战与应用十九)--学习笔记
41 | io包中的接口和工具 (下) 上一篇文章中,我主要讲到了io.Reader的扩展接口和实现类型.当然,io代码包中的核心接口不止io.Reader一个. 我们基于它引出的一条主线,只是io包 ...
- Go语言核心36讲(Go语言实战与应用二十二)--学习笔记
44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...
随机推荐
- 安装Windows_server_2012_r2虚拟机步骤
创建虚拟机 使用Windows_server_2012_r2镜像 网上搜索Windows产品密钥:TVNTG-VFJQ3-FQXFP-DVCP6-D3VJ8 点击完成,等待加载 选择第二个 等待安装 ...
- ESP8266 RTOS SDK开发
ESP8266 RTOS SDK开发 目录 ESP8266 RTOS SDK开发 一.源码RTOS SDK包的下载和编译 二.固件烧录 1.管脚定义 三.程序例程 ## 1.PWM设置 连接MQTT ...
- Spark 读Hive并写入HBase
package com.grady import org.apache.hadoop.hbase.HBaseConfiguration import org.apache.hadoop.hbase.c ...
- 跟羽夏学 Ghidra ——初识
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章 ...
- 算法:KMP, str1字符串是否包含str2字符串
[普通解法]从左到右遍历str1的每一个字符,然后看如果 以当前字符作为第一个字符出发 是否匹配 str2字符串. [KMP算法] 1)生成一个nextArr数组,长度与str2字符串长度一样.i 的 ...
- CentOS7部署FastDFS+nginx模块
软件下载 # 已经事先把所需软件下载好并上传到/usr/local/src目录了 https://github.com/happyfish100/libfastcommon/archive/V1.0. ...
- 分布式文件存储 CephFS的应用场景
块存储 (适合单客户端使用) 典型设备:磁盘阵列,硬盘. 使用场景: a. docker容器.虚拟机远程挂载磁盘存储分配. b. 日志存储. 文件存储 (适合多客户端有目录结构) 典型设备:FTP.N ...
- Springboot配置文件参数使用docker-compose实现动态配置
文章总结; Springboot配置文件中的一些参数可以写成变量的形式,具体变量的值可以从docker-compose.yml文件中设置来获取 在yml文件中,通过${Envirment_variab ...
- Portainer实用教程
Portainer使用 Nginx 容器实现端口转发 在 WordPress 部署完成后,需要在浏览器内输入 IP:端口或域名:端口 的形式访问网站,但我们一般访问应用的时候都是希望不加端口就能访问域 ...
- 使用k8s部署springcloud解决三大问题
1.正式环境使用的话启动时需要指定使用正式的配置文件,这个要咋处理? 解决办法 文章地址:https://www.cnblogs.com/sanduzxcvbnm/p/13262411.html 分析 ...