39 | 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值在刚被真正使用的时候就可以快速地做好准备。

package main

import (
"bytes"
"fmt"
) func main() {
// 示例1。
var contents string
buffer1 := bytes.NewBufferString(contents)
fmt.Printf("The length of new buffer with contents %q: %d\n",
contents, buffer1.Len())
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
contents, buffer1.Cap())
fmt.Println() contents = "12345"
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Println() contents = "67"
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Println() contents = "89"
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer1.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Print("\n\n") // 示例2。
contents = "abcdefghijk"
buffer2 := bytes.NewBufferString(contents)
fmt.Printf("The length of new buffer with contents %q: %d\n",
contents, buffer2.Len())
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
contents, buffer2.Cap())
fmt.Println() n := 10
fmt.Printf("Grow the buffer with %d ...\n", n)
buffer2.Grow(n)
fmt.Printf("The length of buffer: %d\n", buffer2.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
fmt.Print("\n\n") // 示例3。
var buffer3 bytes.Buffer
fmt.Printf("The length of new buffer: %d\n", buffer3.Len())
fmt.Printf("The capacity of new buffer: %d\n", buffer3.Cap())
fmt.Println() contents = "xyz"
fmt.Printf("Write contents %q ...\n", contents)
buffer3.WriteString(contents)
fmt.Printf("The length of buffer: %d\n", buffer3.Len())
fmt.Printf("The capacity of buffer: %d\n", buffer3.Cap())
}

问题 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 文件中写了一个比较完整的示例,你可以去看一看,并揣摩一下。

package main

import (
"bytes"
"fmt"
) func main() {
// 示例1。
contents := "ab"
buffer1 := bytes.NewBufferString(contents)
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
contents, buffer1.Cap())
fmt.Println() unreadBytes := buffer1.Bytes()
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
fmt.Println() contents = "cdefg"
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Println() // 只要扩充一下之前拿到的未读字节切片unreadBytes,
// 就可以用它来读取甚至修改buffer中的后续内容。
unreadBytes = unreadBytes[:cap(unreadBytes)]
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
fmt.Println() value := byte('X')
fmt.Printf("Set a byte in the unread bytes to %v ...\n", value)
unreadBytes[len(unreadBytes)-2] = value
fmt.Printf("The unread bytes of the buffer: %v\n", buffer1.Bytes())
fmt.Println() // 不过,在buffer的内容容器真正扩容之后就无法这么做了。
contents = "hijklmn"
fmt.Printf("Write contents %q ...\n", contents)
buffer1.WriteString(contents)
fmt.Printf("The capacity of buffer: %d\n", buffer1.Cap())
fmt.Println() unreadBytes = unreadBytes[:cap(unreadBytes)]
fmt.Printf("The unread bytes of the buffer: %v\n", unreadBytes)
fmt.Print("\n\n") // 示例2。
// Next方法返回的后续字节切片也存在相同的问题。
contents = "12"
buffer2 := bytes.NewBufferString(contents)
fmt.Printf("The capacity of new buffer with contents %q: %d\n",
contents, buffer2.Cap())
fmt.Println() nextBytes := buffer2.Next(2)
fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
fmt.Println() contents = "34567"
fmt.Printf("Write contents %q ...\n", contents)
buffer2.WriteString(contents)
fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
fmt.Println() // 只要扩充一下之前拿到的后续字节切片nextBytes,
// 就可以用它来读取甚至修改buffer中的后续内容。
nextBytes = nextBytes[:cap(nextBytes)]
fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
fmt.Println() value = byte('X')
fmt.Printf("Set a byte in the next bytes to %v ...\n", value)
nextBytes[len(nextBytes)-2] = value
fmt.Printf("The unread bytes of the buffer: %v\n", buffer2.Bytes())
fmt.Println() // 不过,在buffer的内容容器真正扩容之后就无法这么做了。
contents = "89101112"
fmt.Printf("Write contents %q ...\n", contents)
buffer2.WriteString(contents)
fmt.Printf("The capacity of buffer: %d\n", buffer2.Cap())
fmt.Println() nextBytes = nextBytes[:cap(nextBytes)]
fmt.Printf("The next bytes of the buffer: %v\n", nextBytes)
}

总结

我们结合两篇内容总结一下。与strings.Builder类型不同,bytes.Buffer不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序地读取其中的子序列。

bytes.Buffer类型使用字节切片作为其内容容器,并且会用一个字段实时地记录已读字节的计数。

虽然我们无法直接计算出这个已读计数,但是由于它在Buffer值中起到的作用非常关键,所以我们很有必要去理解它。

无论是读取、写入、截断、导出还是重置,已读计数都是功能实现中的重要一环。

与strings.Builder类型的值一样,Buffer值既可以被手动扩容,也可以进行自动的扩容。除非我们完全确定后续内容所需的字节数,否则让Buffer值自动去扩容就好了。

Buffer值的扩容方法并不一定会为了获得更大的容量,替换掉现有的内容容器,而是先会本着尽量减少内存分配和内容拷贝的原则,对当前的内容容器进行重用。并且,只有在容量实在无法满足要求的时候,它才会去创建新的内容容器。

此外,你可能并没有想到,Buffer值的某些方法可能会造成内容的泄露。这主要是由于这些方法返回的结果值,在一段时期内会与其所属值的内容容器共用同一个底层数组。

如果我们有意或无意地把这些结果值传到了外界,那么外界就有可能通过它们操纵相关联Buffer值的内容。

这属于很严重的数据安全问题。我们一定要避免这种情况的发生。最彻底的做法是,在传出切片这类值之前要做好隔离。比如,先对它们进行深度拷贝,然后再把副本传出去。

思考题

今天的思考题是:对比strings.Builder和bytes.Buffer的String方法,并判断哪一个更高效?原因是什么?

笔记源码

https://github.com/MingsonZheng/go-core-demo

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

Go语言核心36讲(Go语言实战与应用十七)--学习笔记的更多相关文章

  1. Go语言核心36讲(Go语言实战与应用二)--学习笔记

    24 | 测试的基本规则和流程(下) Go 语言是一门很重视程序测试的编程语言,所以在上一篇中,我与你再三强调了程序测试的重要性,同时,也介绍了关于go test命令的基本规则和主要流程的内容.今天我 ...

  2. Go语言核心36讲(Go语言基础知识三)--学习笔记

    03 | 库源码文件 在我的定义中,库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话). 这里的"其他代码" ...

  3. Go语言核心36讲(Go语言实战与应用一)--学习笔记

    23 | 测试的基本规则和流程 (上) 在接下来的日子里,我将带你去学习在 Go 语言编程进阶的道路上,必须掌握的附加知识,比如:Go 程序测试.程序监测,以及 Go 语言标准库中各种常用代码包的正确 ...

  4. Go语言核心36讲(Go语言实战与应用三)--学习笔记

    25 | 更多的测试手法 在本篇文章,我会继续为你讲解更多更高级的测试方法.这会涉及testing包中更多的 API.go test命令支持的,更多标记更加复杂的测试结果,以及测试覆盖度分析等等. 前 ...

  5. Go语言核心36讲(Go语言实战与应用四)--学习笔记

    26 | sync.Mutex与sync.RWMutex 从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包.这会涉及这些代码包的标准用法.使用禁忌.背后原理以及周边的知识. ...

  6. Go语言核心36讲(Go语言实战与应用十二)--学习笔记

    34 | 并发安全字典sync.Map (上) 我们今天再来讲一个并发安全的高级数据结构:sync.Map.众所周知,Go 语言自带的字典类型map并不是并发安全的. 前导知识:并发安全字典诞生史 换 ...

  7. Go语言核心36讲(Go语言实战与应用十四)--学习笔记

    36 | unicode与字符编码 在开始今天的内容之前,我先来做一个简单的总结. Go 语言经典知识总结 在数据类型方面有: 基于底层数组的切片: 用来传递数据的通道: 作为一等类型的函数: 可实现 ...

  8. Go语言核心36讲(Go语言实战与应用十八)--学习笔记

    40 | io包中的接口和工具 (上) 我们在前几篇文章中,主要讨论了strings.Builder.strings.Reader和bytes.Buffer这三个数据类型. 知识回顾 还记得吗?当时我 ...

  9. Go语言核心36讲(Go语言实战与应用二十二)--学习笔记

    44 | 使用os包中的API (上) 我们今天要讲的是os代码包中的 API.这个代码包可以让我们拥有操控计算机操作系统的能力. 前导内容:os 包中的 API 这个代码包提供的都是平台不相关的 A ...

  10. Go语言核心36讲(Go语言实战与应用二十四)--学习笔记

    46 | 访问网络服务 前导内容:socket 与 IPC 人们常常会使用 Go 语言去编写网络程序(当然了,这方面也是 Go 语言最为擅长的事情).说到网络编程,我们就不得不提及 socket. s ...

随机推荐

  1. 2020.11.6-vj补题

    A - A CodeForces - 136A 题解:按输入的顺序输出对应的下标即可,定义一个数组,将输入的作为下标,下标为值,最后依次输出即可: #include<bits/stdc++.h& ...

  2. 小白自制Linux开发板 七. USB驱动配置

    本文章基于https://whycan.com/t_3087.htmlhttps://whycan.com/t_6021.html整理 F1c100s芯片支持USB的OTG模式,也就是可以通过更改Us ...

  3. CSP-J 2021 复赛游记

    Day-1 啥也没干 晚上看了看洛谷的讨论,据说freopen在打开的最后要加 fclose(stdin);fclose(stdout); 不加也可.不过据说Linux在return 0之前不会自动关 ...

  4. 【UE4】基础概念——文件结构、类型、反射、编译、接口、垃圾回收、序列化

    新标签打开或者下载看大图 思维导图 Engine Structure Pipeline Programming Pipeline Blueprint Pipeline

  5. 【数据结构与算法Python版学习笔记】树——平衡二叉搜索树(AVL树)

    定义 能够在key插入时一直保持平衡的二叉查找树: AVL树 利用AVL树实现ADT Map, 基本上与BST的实现相同,不同之处仅在于二叉树的生成与维护过程 平衡因子 AVL树的实现中, 需要对每个 ...

  6. Sequence Model-week1编程题2-Character level language model【RNN生成恐龙名 LSTM生成莎士比亚风格文字】

    Character level language model - Dinosaurus land 为了构建字符级语言模型来生成新的名称,你的模型将学习不同的名字,并随机生成新的名字. 任务清单: 如何 ...

  7. [Beta]the Agiles Scrum Meeting 3

    会议时间:2020.5.14 20:00 1.每个人的工作 今天已完成的工作 成员 已完成的工作 yjy 实现前端界面美化 tq 实现查看.删除测试点功能的前端修复功能中的bug wjx 升级系统实现 ...

  8. 使用flink实现一个简单的wordcount

    使用flink实现一个简单的wordcount 一.背景 二.需求 三.前置条件 1.jdk版本要求 2.maven版本要求 四.实现步骤 1.创建 flink 项目 2.编写程序步骤 1.创建Str ...

  9. Noip模拟75 2021.10.12

    T1 如何优雅的送分 他说是送分题,我就刚,没刚出来,想到莫比乌斯容斥后就都没推出来 好吧还是不能被恶心的题目,挑衅的语言打乱做题节奏 于是这一场也就没了.... $F(i)$表示$i$的不同质因子集 ...

  10. STM32 禁用或开启总中断

    今天把之前自己的一些在中断方面所产生的疑惑把具体的解决办法给大家分享一下,希望能够帮到大家. STM32在使用时有时需要禁用全局中断,比如MCU在升级过程中需禁用外部中断,防止升级过程中外部中断触发导 ...